carlos / platform Public

WIP files seed checkpoint 12 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev f156851fbf92d72597ffb663e47f3e0584f30bb8 parent 211445f
6 files changed, +193 −7
  • internal/host/claimservinghandover_linux_test.go +47 −0
  • internal/host/claimservingpark_transport_linux_test.go +9 −0
  • internal/host/claimservingparkroot_linux.go +7 −1
  • internal/host/claimservingparkroot_linux_test.go +58 −0
  • internal/host/claimservingpeer_rootrunner_linux_test.go +69 −3
  • internal/provision/testdata/plan14-root-reads-seed.tsv +3 −3

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	335.071s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.031s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.210s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.709s
ok  	github.com/carlosframework/platform/internal/activator	235.384s
ok  	github.com/carlosframework/platform/internal/agentsock	0.023s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.298s
ok  	github.com/carlosframework/platform/internal/apperr	0.077s
ok  	github.com/carlosframework/platform/internal/applog	0.132s
ok  	github.com/carlosframework/platform/internal/awsinv	0.008s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.009s
ok  	github.com/carlosframework/platform/internal/boxid	0.689s
?   	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.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.698s
ok  	github.com/carlosframework/platform/internal/changelog	0.290s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	362.628s
?   	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.818s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.105s
ok  	github.com/carlosframework/platform/internal/doorkey	0.008s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	25.978s
?   	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.127s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.019s
ok  	github.com/carlosframework/platform/internal/geo	0.018s
ok  	github.com/carlosframework/platform/internal/geoip	0.032s
ok  	github.com/carlosframework/platform/internal/host	148.037s
ok  	github.com/carlosframework/platform/internal/hostactors	0.275s
ok  	github.com/carlosframework/platform/internal/hostbox	2.253s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.494s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.785s
ok  	github.com/carlosframework/platform/internal/hostfence	0.035s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.498s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	1.007s
ok  	github.com/carlosframework/platform/internal/hostops	1.431s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.005s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.455s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.136s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.005s
ok  	github.com/carlosframework/platform/internal/instancectl	1.813s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.689s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.007s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	1.417s
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.018s
ok  	github.com/carlosframework/platform/internal/mail	0.158s
ok  	github.com/carlosframework/platform/internal/maintenance	0.085s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.502s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.081s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	20.910s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.736s
ok  	github.com/carlosframework/platform/internal/objstore	0.169s
ok  	github.com/carlosframework/platform/internal/ops	0.158s
ok  	github.com/carlosframework/platform/internal/park	0.196s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	11.289s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.009s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.035s
ok  	github.com/carlosframework/platform/internal/relay	7.219s
ok  	github.com/carlosframework/platform/internal/release	19.342s
ok  	github.com/carlosframework/platform/internal/rollout	3.619s
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.008s
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.282s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.726s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.612s
ok  	github.com/carlosframework/platform/internal/status	0.041s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.285s
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.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.543s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/14 18:55:41 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7A4598E3803E, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/internal/host/claimservinghandover_linux_test.go b/internal/host/claimservinghandover_linux_test.go
index a7ab85c95..5e3cf4fa3 100644
--- a/internal/host/claimservinghandover_linux_test.go
+++ b/internal/host/claimservinghandover_linux_test.go
@@ -89,6 +89,53 @@ func servingHandoverRoot(t *testing.T, f *servingRootFixture) ClaimRootHTTP {
func TestServingParkColdHandoverPreservesState(t *testing.T) { runServingColdHandoverBoundary(t, nil) }
+func TestServingHandoverThenPrelaunchParkReplay(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ root := servingHandoverRoot(t, f)
+ socket := servingRootSocket(t, root)
+ hook := coldHandoverWith(handoverRecoveryIdentity(t, f.actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
+ return hostguard.FenceReceipt{Target: f.receipt.Target}, nil
+ })
+ admitted := f.f.read()
+ for i := 0; i < 2; i++ {
+ if err := hook(t.Context(), f.predecessor, admitted); err != nil {
+ t.Fatal("handover", err)
+ }
+ }
+ installed, err := f.f.guard.Capture(f.current.Host)
+ if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) {
+ t.Fatalf("installation: %+v %v", installed, err)
+ }
+ target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"})
+ if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
+ t.Fatal(err)
+ }
+ client := hostbox.ServingParkClient{Call: rootJournalClient(socket)}
+ a, err := client.Park(t.Context(), f.current, f.prior)
+ if err != nil {
+ t.Fatal("park after prelaunch", err)
+ }
+ f.tags(f.current.Stamp)
+ if err := client.Complete(t.Context(), a.ID, f.current); err != nil {
+ t.Fatal(err)
+ }
+ before, err := f.p.ledger.read(a.ID)
+ if err != nil || !before.Complete {
+ t.Fatal("completion", err)
+ }
+ again, err := client.Park(t.Context(), f.current, f.prior)
+ if err != nil || again != a {
+ t.Fatalf("replay after handover and prelaunch: %+v %v", again, err)
+ }
+ after, err := f.p.ledger.read(a.ID)
+ if err != nil || !reflect.DeepEqual(before, after) {
+ t.Fatal("replay changed record", err)
+ }
+ if err := f.p.ledger.fs.CheckParked(&after); err != nil {
+ t.Fatal(err)
+ }
+}
+
func runServingColdHandoverBoundary(t *testing.T, mount func(*testing.T, string), revisions ...uint64) {
t.Helper()
f := newServingRootFixture(t, true)
diff --git a/internal/host/claimservingpark_transport_linux_test.go b/internal/host/claimservingpark_transport_linux_test.go
index d36c268b6..c5194ca09 100644
--- a/internal/host/claimservingpark_transport_linux_test.go
+++ b/internal/host/claimservingpark_transport_linux_test.go
@@ -106,6 +106,15 @@ func TestServingParkSocketRetiresAndCompletes(t *testing.T) {
if err != nil || !r.Complete {
t.Fatalf("completion: %+v %v", r, err)
}
+ if !legacy {
+ again, err := client.Park(t.Context(), f.current, f.prior)
+ if err != nil || again != a {
+ t.Fatalf("fresh request after completion: %+v %v", again, err)
+ }
+ if exact.ID == parkRequest.ID {
+ t.Fatal("fresh request reused cached transport reply")
+ }
+ }
before, err := f.disk.Read("retirement-" + a.ID + ".json")
if err != nil {
t.Fatal(err)
diff --git a/internal/host/claimservingparkroot_linux.go b/internal/host/claimservingparkroot_linux.go
index 97a3d2e4e..1ea58b509 100644
--- a/internal/host/claimservingparkroot_linux.go
+++ b/internal/host/claimservingparkroot_linux.go
@@ -181,7 +181,13 @@ func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn
return err
}
if requestedPrior != (servingArtifactIdentity{}) {
- prior, e := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation)
+ stamp := tags
+ if tags == current.Stamp {
+ // Retagging has consumed the old pair. An original request
+ // still selects the predecessor saved before that change.
+ stamp = active.TagsFrom
+ }
+ prior, e := servingPredecessor(o.Claim, current, stamp, requestedPrior.Generation)
if e != nil || prior != requestedPrior {
return hostbox.ErrChanged
}
diff --git a/internal/host/claimservingparkroot_linux_test.go b/internal/host/claimservingparkroot_linux_test.go
index 479e845e8..4b0b588e7 100644
--- a/internal/host/claimservingparkroot_linux_test.go
+++ b/internal/host/claimservingparkroot_linux_test.go
@@ -68,6 +68,7 @@ type servingRootFixture struct {
actor hostbox.Actor
current, prior servingArtifactIdentity
admitted hostclaim.Observed
+ predecessor hostclaim.Observed
receipt hostguard.FenceReceipt
db string
trace []string
@@ -118,6 +119,7 @@ func newServingRootFixture(t *testing.T, launched bool) *servingRootFixture {
}
}
o := f.read()
+ fx.predecessor = o
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)
@@ -237,6 +239,58 @@ func TestServingParkRootGenerationAuthority(t *testing.T) {
})
}
}
+
+func TestServingParkRootReplaysOriginalSelectorAfterRetag(t *testing.T) {
+ for _, complete := range []bool{false, true} {
+ t.Run(fmt.Sprintf("complete=%v", complete), func(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ a, err := f.park()
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.tags(f.current.Stamp)
+ if complete {
+ if err := f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil {
+ t.Fatal(err)
+ }
+ }
+ before, err := f.p.ledger.read(a.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.f.now = f.f.now.Add(time.Hour)
+ again, err := f.park()
+ if err != nil || again != a {
+ t.Fatalf("original selector after retag: %+v %v", again, err)
+ }
+ for _, field := range []string{"generation", "stamp", "session", "box", "incarnation"} {
+ bad := f.prior
+ switch field {
+ case "generation":
+ bad.Generation = "foreign"
+ case "stamp":
+ bad.Stamp = f.current.Stamp
+ case "session":
+ bad.Session = f.current.Session
+ case "box":
+ bad.Box = "foreign"
+ case "incarnation":
+ bad.Incarnation = "foreign"
+ }
+ if _, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, bad); err == nil {
+ t.Fatalf("accepted changed %s", field)
+ }
+ }
+ after, err := f.p.ledger.read(a.ID)
+ if err != nil || !reflect.DeepEqual(before, after) {
+ t.Fatalf("replay changed retirement: %+v %v", after, err)
+ }
+ if err := f.p.ledger.fs.CheckParked(&after); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
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) {
@@ -502,6 +556,10 @@ func TestServingParkRootContinuesOriginalRetirement(t *testing.T) {
if err != nil || again != continued {
t.Fatalf("replay: %+v %v", again, err)
}
+ again, err = f.p.parkServingDatabase(t.Context(), f.conn, f.current, requested)
+ if err != nil || again != continued {
+ t.Fatalf("original continuation selector: %+v %v", again, err)
+ }
})
}
}
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
index 85033015e..c2e348617 100644
--- a/internal/host/claimservingpeer_rootrunner_linux_test.go
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -13,6 +13,7 @@ import (
"os/exec"
"os/user"
"path/filepath"
+ "reflect"
"strconv"
"strings"
"sync/atomic"
@@ -34,6 +35,9 @@ type servingPeerCommand struct {
Current, Prior servingArtifactIdentity
Target hostguard.FenceTarget
ID string
+ Previous hostclaim.Observed
+ Admitted hostclaim.Observed
+ Actor hostbox.Actor
}
type servingPeerReply struct {
@@ -97,6 +101,11 @@ func TestRootServingParkPeerPayload(t *testing.T) {
var reply servingPeerReply
var callErr error
switch q.Action {
+ case "handover":
+ hook := coldHandoverWith(handoverRecoveryIdentity(t, q.Actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
+ return hostguard.FenceReceipt{Target: q.Target}, nil
+ })
+ callErr = hook(t.Context(), q.Previous, q.Admitted)
case "park":
reply.Authorization, callErr = client.Park(t.Context(), q.Current, q.Prior)
case "complete":
@@ -175,6 +184,15 @@ func TestRootServingParkPeerPayload(t *testing.T) {
// Claim, installation, predecessor stop/start and roster/census data are fixtures;
// peer authentication, prelaunch containment, park authority and IO are real.
func TestRootServingParkAuthenticatedPeer(t *testing.T) {
+ runServingParkAuthenticatedPeer(t, false)
+}
+
+func TestRootServingParkAuthenticatedHandover(t *testing.T) {
+ runServingParkAuthenticatedPeer(t, true)
+}
+
+func runServingParkAuthenticatedPeer(t *testing.T, handover bool) {
+ t.Helper()
servingRootRunner(t)
pid1, err := os.ReadFile("/proc/1/comm")
if err != nil || strings.TrimSpace(string(pid1)) != "systemd" {
@@ -240,9 +258,20 @@ func TestRootServingParkAuthenticatedPeer(t *testing.T) {
}
f := newServingRootFixture(t, true)
servingRunnerMount(t, f.f.guard.DataDir)
- // Refresh the state inventory after the bind mount. This remains a fixture
- // installation; it does not exercise the production installation door.
- f.install(f.admitted)
+ for name, body := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
+ path := filepath.Join(filepath.Dir(f.db), name)
+ if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(body), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if handover {
+ servingHandoverRoot(t, f)
+ } else {
+ f.install(f.admitted)
+ }
state, err := rootstate.Open(filepath.Join(root, "parks"), true, false)
if err != nil {
t.Fatal(err)
@@ -368,6 +397,7 @@ func TestRootServingParkAuthenticatedPeer(t *testing.T) {
}
command := func(q servingPeerCommand, refused bool) servingPeerReply {
t.Helper()
+ t.Logf("authenticated peer request: action=%s expect-refusal=%v", q.Action, refused)
if err := encode.Encode(q); err != nil {
t.Fatal(err)
}
@@ -406,6 +436,36 @@ func TestRootServingParkAuthenticatedPeer(t *testing.T) {
t.Fatalf("refusal wrote park authority: %s", name)
}
}
+ if handover {
+ transfer := servingPeerCommand{Action: "handover", Previous: f.predecessor, Admitted: f.f.read(), Actor: f.actor, Target: f.receipt.Target}
+ captured := hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp}
+ oldInventory, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.current.Host, captured)
+ if err != nil {
+ t.Fatal(err)
+ }
+ admitted.Store(false)
+ command(transfer, true)
+ admitted.Store(true)
+ bad := transfer
+ bad.Target.FenceID = "foreign-receipt"
+ command(bad, true)
+ command(transfer, false)
+ command(transfer, false)
+ installed, err := f.f.guard.Capture(f.current.Host)
+ if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) {
+ t.Fatalf("authenticated handover installation: %+v %v", installed, err)
+ }
+ retained, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.current.Host, captured)
+ if err != nil || !reflect.DeepEqual(retained, oldInventory) {
+ t.Fatal("handover changed predecessor inventory", err)
+ }
+ for suffix, want := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} {
+ body, err := os.ReadFile(f.db + suffix)
+ if err != nil || string(body) != want {
+ t.Fatal("handover changed source", suffix, err)
+ }
+ }
+ }
target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor})
pre := command(servingPeerCommand{Action: "prelaunch", Target: target}, false)
actual, err := f.f.journal.Read(t.Context(), pre.Launch.Target)
@@ -438,6 +498,12 @@ func TestRootServingParkAuthenticatedPeer(t *testing.T) {
if replay := command(q, false).Authorization; replay != parked {
t.Fatal("authenticated replay changed retirement")
}
+ for name, want := range map[string]string{"files/upload": "tenant upload", "env": "tenant configuration"} {
+ body, err := os.ReadFile(filepath.Join(filepath.Dir(f.db), name))
+ if err != nil || string(body) != want {
+ t.Fatal("retirement changed preserved state", name, err)
+ }
+ }
}
// Transfer the serving installation on the nested mount without moving its
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 84e757af5..6525f6fad 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,4 +1,4 @@
-# Files seed checkpoint 11 overlay; pending Paul’s census ruling.
+# Files seed checkpoint 12 overlay; pending Paul’s census ruling.
# Database retirement activation remains refused; serving installation transfer uses the existing root transport.
# 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.
@@ -69,13 +69,13 @@ internal/host/claimservingparksource_linux.go servingParkStateDirectory authorit
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; live edge peer reauthentication under namespace and host locks before census, inventory and final claim checks; root request entry refuses nil connections d3582f0760b4459721b3e6040cda738b105c1da19cc5d0746d04fea26e313b5c
internal/host/claimservingparkroot_linux.go servingRootStops authority C,J4 rev3=PROPOSED-seed; retained exact root serving receipts and known retired installation; same enrollment, session and region, with canonical recorded revision no newer than the active actor d371f01614c0bb64d3f412123fc9a90b8bee43d02b6c48e67fc89d9a35187005
-internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired tags, root slot/record and retained admissions; recorded legacy work dispatches to separately locked fresh legacy authority 61c29d5b37168ab053499553eaf0052fcee134a601c86acb3195fad416d28247
+internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired tags, root slot/record and retained admissions; post-retag replay selects the predecessor from the saved active tag-source stamp and revalidates its exact identity; recorded legacy work dispatches to separately locked fresh legacy authority 4a1d89b8f65ed52e1aedf00595021ec9b42bfd1fed4a3466a512fbdd6078538c
internal/host/claimservingparkroot_linux.go servingParkRoot.completeServingDatabasePark authority C,J4 rev3=PROPOSED-seed; fresh paired enrollment, same current writer, root source and original/continuation stop history; legacy additionally authenticates persistent no-process or admitted predecessor evidence 8ca9bdd3c1209c6d5257bc07a7d12d0bab900e9c2911d0698077eb2116e5123c
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 8b2636a4c0d76b826c438e30b4a433ebf2ce5195e9d6aa21980b72a9bf84caa3
+@coverage internal/host 3b69facca859268158f6e90dc5ff978225d200725716332ae0235ba94a415a5c
@coverage internal/hostbox eb1f4980ba609cc886ae4bd73aedbb6be3fb2eb104165bb1d8404da0814ed190
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
internal/host/claimservinglegacy.go servingLegacyKey authority C,J4 rev3=PROPOSED-seed; box, host and incarnation derive private stop and consumption keys; never a caller path 93c10e40202ab23986b54a366edd7abfab9f8d942736305b5732e8741e96f3ab