carlos / platform Public

WIP files seed checkpoint 31 (not for merge)

Reboot handover race fixed (persistent identity across renumbering); FS-R11 Restore CAS observation threaded to Materialize, foreign writes refused; 34 root tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev f0863053aa35f354241a43a47f25fcda3991ed11 parent fb12595
26 files changed, +779 −56
  • internal/activator/claimwake.go +11 −10
  • internal/activator/servingretry.go +16 −4
  • internal/activator/servingretry_test.go +47 −0
  • internal/host/claimfilesseed_isolated_rootrunner_linux_test.go +3 −0
  • internal/host/claimfilesseed_reboot_rootrunner_linux_test.go +30 −0
  • internal/host/claimfilesseed_renumber_rootrunner_linux_test.go +104 −0
  • internal/host/claimfilesseed_restore_linux_test.go +11 −1
  • internal/host/claimfilesseed_twohost_rootrunner_linux_test.go +1 −1
  • internal/host/claimrestore_observation.go +93 −0
  • internal/host/claimrestore_observation_linux_test.go +133 −0
  • internal/host/claimretire_local.go +5 −1
  • internal/host/claimservinghandover.go +1 −1
  • internal/host/claimservinghandover_reboot_linux.go +74 −0
  • internal/host/claimservinghandover_reboot_linux_test.go +62 −0
  • internal/host/claimservinghandover_reboot_other.go +13 −0
  • internal/host/claimservingretire.go +8 −1
  • internal/host/claimwakeassembly.go +11 −4
  • internal/host/plan12_restore_linux_test.go +19 −2
  • internal/host/plan9_d4_retire_linux_test.go +6 −1
  • internal/hostbox/servinghandover.go +30 −7
  • internal/hostbox/servinghandover_linux_test.go +53 −1
  • internal/park/local.go +9 −8
  • internal/provision/plan14_legacy_identity_test.go +8 −3
  • internal/provision/plan14_root_reads_test.go +1 −1
  • internal/provision/plan9_b8_census_test.go +1 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +29 −10

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	290.534s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.666s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.176s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.702s
ok  	github.com/carlosframework/platform/internal/activator	250.427s
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.480s
ok  	github.com/carlosframework/platform/internal/apperr	0.069s
ok  	github.com/carlosframework/platform/internal/applog	0.128s
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.834s
?   	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.697s
ok  	github.com/carlosframework/platform/internal/changelog	0.315s
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	389.775s
?   	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.619s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.325s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	26.505s
?   	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.129s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/host	161.102s
ok  	github.com/carlosframework/platform/internal/hostactors	0.286s
ok  	github.com/carlosframework/platform/internal/hostbox	2.358s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.493s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	4.740s
ok  	github.com/carlosframework/platform/internal/hostfence	0.033s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.451s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.018s
ok  	github.com/carlosframework/platform/internal/hostguard	0.978s
ok  	github.com/carlosframework/platform/internal/hostops	1.553s
?   	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.465s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.146s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	4.163s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.716s
ok  	github.com/carlosframework/platform/internal/instancefs	0.004s
ok  	github.com/carlosframework/platform/internal/instancekey	0.003s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.007s
ok  	github.com/carlosframework/platform/internal/layout	1.506s
ok  	github.com/carlosframework/platform/internal/lease	9.785s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.014s
ok  	github.com/carlosframework/platform/internal/mail	0.166s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.534s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.077s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	20.909s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.806s
ok  	github.com/carlosframework/platform/internal/objstore	0.189s
ok  	github.com/carlosframework/platform/internal/ops	0.176s
ok  	github.com/carlosframework/platform/internal/park	0.203s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	16.266s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.873s
ok  	github.com/carlosframework/platform/internal/relay	7.229s
ok  	github.com/carlosframework/platform/internal/release	19.387s
ok  	github.com/carlosframework/platform/internal/rollout	3.724s
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.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.024s
ok  	github.com/carlosframework/platform/internal/runs	0.575s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.461s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.245s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.010s
ok  	github.com/carlosframework/platform/internal/status	0.047s
ok  	github.com/carlosframework/platform/internal/steering	0.009s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.211s
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.003s
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.006s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.558s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 46989474 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/15 23:36:59 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DED833FBEA2B7D, 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/activator/claimwake.go b/internal/activator/claimwake.go
index 81b6616ee..28344498f 100644
--- a/internal/activator/claimwake.go
+++ b/internal/activator/claimwake.go
@@ -113,16 +113,17 @@ type ClaimStart struct {
// OpenProbe journals the first app-start time before execution and returns
// that same time on retries, including after controller restart. Its
// provider binds the moves attempt and admission, never resets Opened.
- OpenProbe func(context.Context, time.Time) (time.Time, error)
- Work hostwrite.Work
- Identity hostwrite.Identity
- Manager *cgroup.Manager
- Host string
- Limit cgroup.Limit
- Restore func(context.Context) error
- Materialize func(context.Context) error
- Build func() *exec.Cmd
- Ready func(context.Context, *exec.Cmd) error
+ OpenProbe func(context.Context, time.Time) (time.Time, error)
+ Work hostwrite.Work
+ Identity hostwrite.Identity
+ Manager *cgroup.Manager
+ Host string
+ Limit cgroup.Limit
+ Restore func(context.Context) error
+ Materialize func(context.Context) error
+ RetryClaimWrite func(hostclaim.Observed, hostclaim.Observed) error
+ Build func() *exec.Cmd
+ Ready func(context.Context, *exec.Cmd) error
// Reserve(0) validates the already reserved first attempt. Later calls CAS.
Reserve func(context.Context, int) error
Failed func(context.Context, int) error
diff --git a/internal/activator/servingretry.go b/internal/activator/servingretry.go
index d0ab0c193..3fe0b6007 100644
--- a/internal/activator/servingretry.go
+++ b/internal/activator/servingretry.go
@@ -39,7 +39,7 @@ func retryOf(o hostclaim.Observed) *hostclaim.WakeRetry {
return o.Claim.Holder.WakeRetry
}
-func (d ServingRetries) settle(ctx context.Context, expected hostclaim.Observed) error {
+func (d ServingRetries) settle(ctx context.Context, expected hostclaim.Observed, writes ...func(hostclaim.Observed, hostclaim.Observed) error) error {
r := retryOf(expected)
if r == nil || r.State != "attempting" || r.Attempt == nil {
return nil
@@ -60,7 +60,16 @@ func (d ServingRetries) settle(ctx context.Context, expected hostclaim.Observed)
if err != nil {
return err
}
- _, err = hostclaim.Update(ctx, d.Claims, o, hostclaim.FailWakeRetryAttempt(hostclaim.WakeRetryResult{HolderIncarnation: expected.Claim.Holder.Incarnation, EpisodeID: r.EpisodeID, Attempt: *r.Attempt, Now: d.Now()}))
+ written, err := hostclaim.Update(ctx, d.Claims, o, hostclaim.FailWakeRetryAttempt(hostclaim.WakeRetryResult{HolderIncarnation: expected.Claim.Holder.Incarnation, EpisodeID: r.EpisodeID, Attempt: *r.Attempt, Now: d.Now()}))
+ if err == nil {
+ for _, write := range writes {
+ if write != nil {
+ if err := write(o, written); err != nil {
+ return err
+ }
+ }
+ }
+ }
return err
}
@@ -205,9 +214,12 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
return hostbox.ErrChanged
}
next := d.attempt(inputs, o.Claim.Holder.Session)
- _, err = hostclaim.Update(ctx, d.Claims, fresh, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: inc, EpisodeID: episode, Attempt: next, Now: d.Now()}))
+ written, err := hostclaim.Update(ctx, d.Claims, fresh, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: inc, EpisodeID: episode, Attempt: next, Now: d.Now()}))
if err == nil {
attempt = next
+ if s.RetryClaimWrite != nil {
+ return s.RetryClaimWrite(fresh, written)
+ }
}
return err
}
@@ -222,7 +234,7 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
if rr == nil || rr.EpisodeID != episode || rr.Attempt == nil || *rr.Attempt != attempt {
return hostbox.ErrChanged
}
- return d.settle(recovery, fresh)
+ return d.settle(recovery, fresh, s.RetryClaimWrite)
}
s.Healthy = func(ctx context.Context, _ int, cmd *exec.Cmd) error {
if cmd == nil || cmd.Process == nil {
diff --git a/internal/activator/servingretry_test.go b/internal/activator/servingretry_test.go
index ce4e09e8b..8ade977f8 100644
--- a/internal/activator/servingretry_test.go
+++ b/internal/activator/servingretry_test.go
@@ -237,3 +237,50 @@ func TestServingSeedRerunSettlesWithoutReservation(t *testing.T) {
})
}
}
+
+func TestServingRetriesReturnOwnClaimWrites(t *testing.T) {
+ st, o, base, d, now, _ := servingRetryFixture(t)
+ var expected hostclaim.Observed
+ calls := 0
+ base.Prepare = func(_ context.Context, _ registry.Route, o hostclaim.Observed, _ StartPolicy) (ClaimStart, error) {
+ expected = o
+ return ClaimStart{RetryClaimWrite: func(before, after hostclaim.Observed) error {
+ if before.ETag != expected.ETag || after.ETag == before.ETag || after.Claim.Gen != before.Claim.Gen+1 {
+ t.Fatal("write did not chain from Prepare")
+ }
+ expected = after
+ calls++
+ return nil
+ }}, nil
+ }
+ runtime, err := d.Wrap(base)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rt := registry.Route{Host: o.Host}
+ admitted, err := runtime.Admit(t.Context(), rt, o, StartPolicy{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ start, err := runtime.Prepare(t.Context(), rt, admitted, StartPolicy{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := start.Reserve(t.Context(), 0); err != nil {
+ t.Fatal(err)
+ }
+ if calls != 0 {
+ t.Fatal("first reservation invented a write")
+ }
+ if err := start.Failed(t.Context(), 0); err != nil {
+ t.Fatal(err)
+ }
+ *now = now.Add(2 * time.Second)
+ if err := start.Reserve(t.Context(), 1); err != nil {
+ t.Fatal(err)
+ }
+ actual, err := hostclaim.Read(t.Context(), st, o.Host)
+ if err != nil || calls != 2 || expected.ETag != actual.ETag {
+ t.Fatal("lost retry write", calls, err)
+ }
+}
diff --git a/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
index 4049c0c1d..28e4a162e 100644
--- a/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
@@ -94,6 +94,9 @@ func rootFilesSeedIsolatedWitness(t *testing.T, binary string, twoHosts ...bool)
t.Fatal("refuse existing fixture path", dir, err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
+ if dir == "/data" && strings.HasPrefix(t.Name(), "TestRootRebootFilesSeedTwoHostRenumber") {
+ seedRunnerRenumberDisk(t)
+ }
}
account, err := user.Lookup("carlos")
if err != nil {
diff --git a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
index a874e603e..37c8a6460 100644
--- a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
@@ -105,6 +105,20 @@ func saveFilesSeedReboot(t *testing.T, binary string, hosts []*seedLiveHost) {
t.Fatal(err)
}
if r.Authorization.Successor.Host == host && r.Parked && !r.Closed {
+ if strings.Contains(t.Name(), "Renumber") {
+ // Loop devices have no independent volume serial. Expire the
+ // retained park before reboot; FS-R7 forbids its deletion after
+ // renumber. The ordinary EBS pair retains its parks across boot.
+ f.f.now = r.ReapAfter.Add(time.Second)
+ if err := f.p.ledger.withNamespace(t.Context(), func() error { return f.p.ledger.reapLocked(t.Context(), r.Authorization.ID) }); err != nil {
+ t.Fatal("expire loop park before reboot", err)
+ }
+ r, err = f.p.ledger.readHistorical(r.Authorization.ID)
+ if err != nil || !r.Closed {
+ t.Fatal("expired loop park", err)
+ }
+ t.Log("loop park expired before reboot", host)
+ }
sh.Records = append(sh.Records, r)
}
}
@@ -200,6 +214,22 @@ func TestRootRebootFilesSeedTwoHostPhase2(t *testing.T) {
for _, savedHost := range saved.Hosts {
h := reopenSeedLiveHost(t, savedHost, witness, state)
hosts = append(hosts, h)
+ inventory, err := h.p.f.p.root.Journals.Inventory.ReadStopInventory(t.Context(), savedHost.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var current syscall.Stat_t
+ if err := syscall.Stat(filepath.Dir(h.p.f.db), &current); err != nil {
+ t.Fatal(err)
+ }
+ for _, entry := range inventory.Entries {
+ if entry.Kind == "path" {
+ if strings.Contains(t.Name(), "Renumber") && entry.Device == uint64(current.Dev) {
+ t.Fatal("real filesystem device did not renumber")
+ }
+ t.Logf("reboot directory host=%s recordedDevice=%d currentDevice=%d recordedInode=%d currentInode=%d", savedHost.Host, entry.Device, current.Dev, entry.Inode, current.Ino)
+ }
+ }
h.wake(t)
}
for _, h := range hosts {
diff --git a/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go b/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go
new file mode 100644
index 000000000..d6a54c6bd
--- /dev/null
+++ b/internal/host/claimfilesseed_renumber_rootrunner_linux_test.go
@@ -0,0 +1,104 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "golang.org/x/sys/unix"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestRootRebootFilesSeedTwoHostRenumberPhase1(t *testing.T) {
+ TestRootRebootFilesSeedTwoHostPhase1(t)
+}
+func TestRootRebootFilesSeedTwoHostRenumberPhase2(t *testing.T) {
+ TestRootRebootFilesSeedTwoHostPhase2(t)
+}
+
+func seedRunnerRenumberDisk(t *testing.T) {
+ t.Helper()
+ command := func(args ...string) string {
+ t.Helper()
+ out, err := exec.CommandContext(t.Context(), args[0], args[1:]...).CombinedOutput()
+ if err != nil {
+ t.Fatalf("renumber disk %q: %v %s", args, err, out)
+ }
+ return strings.TrimSpace(string(out))
+ }
+ create := func(path string, size int64) {
+ t.Helper()
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = f.Truncate(size)
+ closeErr := f.Close()
+ if err != nil || closeErr != nil {
+ t.Fatal(err, closeErr)
+ }
+ }
+ detach := func(device string) {
+ t.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ if out, err := exec.CommandContext(ctx, "losetup", "--detach", device).CombinedOutput(); err != nil {
+ t.Errorf("detach %s: %v %s", device, err, out)
+ }
+ })
+ }
+ image := filepath.Join(seedRebootPath, "data.ext4")
+ deviceFile := filepath.Join(seedRebootPath, "data.device")
+ first := strings.HasSuffix(t.Name(), "Phase1")
+ if first {
+ create(image, 2<<30)
+ command("mkfs.ext4", "-q", "-F", image)
+ } else {
+ old, err := os.ReadFile(deviceFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ suffix := strings.TrimPrefix(string(old), "/dev/loop")
+ if suffix == string(old) {
+ t.Fatal("invalid loop device")
+ }
+ if _, err := strconv.ParseUint(suffix, 10, 32); err != nil {
+ t.Fatal(err)
+ }
+ blocker := filepath.Join(seedRebootPath, "blocker")
+ create(blocker, 1<<20)
+ command("losetup", string(old), blocker)
+ detach(string(old))
+ }
+ device := command("losetup", "--find", "--show", image)
+ detach(device)
+ if first {
+ if err := os.WriteFile(deviceFile, []byte(device), 0600); err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ old, err := os.ReadFile(deviceFile)
+ if err != nil || string(old) == device {
+ t.Fatal("device number did not change", err)
+ }
+ }
+ if err := unix.Mount(device, "/data", "ext4", unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := unix.Unmount("/data", 0); err != nil {
+ t.Error(err)
+ }
+ })
+ t.Logf("two-host data filesystem attached at %s", device)
+}
+
+func TestRootServingHandoverDirectoryProof(t *testing.T) {
+ servingRootRunner(t)
+ TestServingHandoverRebootDirectoryProof(t)
+}
diff --git a/internal/host/claimfilesseed_restore_linux_test.go b/internal/host/claimfilesseed_restore_linux_test.go
index f1d8405e8..57a8c0b00 100644
--- a/internal/host/claimfilesseed_restore_linux_test.go
+++ b/internal/host/claimfilesseed_restore_linux_test.go
@@ -21,6 +21,7 @@ import (
"github.com/carlosframework/platform/internal/genselect"
"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/lease"
"github.com/carlosframework/platform/internal/lease/leasetest"
@@ -67,7 +68,16 @@ func newServingPreparation(t *testing.T, f *servingRootFixture, boot, controller
hook := coldAdmissionHook(f.f.guard, handoverRecoveryIdentity(t, f.actor), servingRootSocket(t, root))
w.AfterAdmission = func(ctx context.Context, prior, admitted hostclaim.Observed) error {
p.events = append(p.events, "admit")
- return hook(ctx, prior, admitted)
+ err := hook(ctx, prior, admitted)
+ if err != nil {
+ old := prior.Claim.Holder
+ receipt, receiptErr := f.f.guard.CompletedColdPredecessor(ctx, prior.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, old.Session, prior.Claim.Op.ID, prior.Claim.Op.Phase)
+ inventory, invErr := root.Reconcile.Inventory.ReadInstallation(ctx, prior.Host, old.Incarnation, old.Stamp)
+ var st syscall.Stat_t
+ statErr := syscall.Stat(filepath.Dir(f.db), &st)
+ t.Logf("handover diagnosis prior=%+v next=%+v receipt=%+v receiptErr=%v inventory=%+v invErr=%v liveDev=%d liveIno=%d statErr=%v", old, admitted.Claim.Holder, receipt.Target, receiptErr, inventory, invErr, st.Dev, st.Ino, statErr)
+ }
+ return err
}
w.Prelaunch = func(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs) (*activator.RoleLaunch, error) {
p.events = append(p.events, "prelaunch")
diff --git a/internal/host/claimfilesseed_twohost_rootrunner_linux_test.go b/internal/host/claimfilesseed_twohost_rootrunner_linux_test.go
index c187f62e5..2545d61a2 100644
--- a/internal/host/claimfilesseed_twohost_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_twohost_rootrunner_linux_test.go
@@ -86,7 +86,7 @@ func rootFilesSeedTwoHost(t *testing.T, binary string) {
for _, h := range hosts {
h.stop()
}
- if t.Name() == "TestRootRebootFilesSeedTwoHostPhase1" {
+ if strings.HasPrefix(t.Name(), "TestRootRebootFilesSeedTwoHost") && strings.HasSuffix(t.Name(), "Phase1") {
saveFilesSeedReboot(t, binary, hosts)
}
}
diff --git a/internal/host/claimrestore_observation.go b/internal/host/claimrestore_observation.go
new file mode 100644
index 000000000..a029455e8
--- /dev/null
+++ b/internal/host/claimrestore_observation.go
@@ -0,0 +1,93 @@
+package host
+
+import (
+ "maps"
+ "path/filepath"
+ "reflect"
+ "sync"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+)
+
+type claimRestoreObservation struct {
+ mu sync.Mutex
+ observed hostclaim.Observed
+ box, dataDir, db string
+}
+
+func sameClaimObservation(a, b hostclaim.Observed) bool {
+ return !a.Absent && !b.Absent && a.Host == b.Host && a.ETag != "" && a.ETag == b.ETag && reflect.DeepEqual(a.Claim, b.Claim)
+}
+
+func (in ClaimLaunchInputs) observeClaimWrite(before, after hostclaim.Observed) error {
+ if in.claimWrite == nil || before.Claim == nil && after.Claim == nil {
+ return nil
+ }
+ return in.claimWrite(before, after)
+}
+
+func (r *claimRestoreObservation) snapshot() hostclaim.Observed {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.observed
+}
+
+func (r *claimRestoreObservation) retag(before, after hostclaim.Observed) error {
+ return r.advance(before, after, func(expected *hostclaim.Claim) error {
+ db, err := filepath.Rel(r.dataDir, r.db)
+ if err != nil || !filepath.IsLocal(db) || expected.Holder == nil {
+ return hostclaim.ErrConflict
+ }
+ db = filepath.ToSlash(db)
+ entry := expected.Inventory[r.box]
+ old, left := entry.Paths[db]
+ marker, right := entry.Paths[db+".gen"]
+ if left != right || old != marker || left && (old == 0 || old >= expected.Holder.Stamp) {
+ return hostclaim.ErrConflict
+ }
+ expected.Inventory = maps.Clone(expected.Inventory)
+ if expected.Inventory == nil {
+ expected.Inventory = make(map[string]hostclaim.InventoryEntry)
+ }
+ entry.Paths = maps.Clone(entry.Paths)
+ if entry.Paths == nil {
+ entry.Paths = make(map[string]uint64)
+ }
+ if expected.Op.Kind == hostclaim.KindServing {
+ entry.Layout = "nested"
+ }
+ entry.Paths[db], entry.Paths[db+".gen"] = expected.Holder.Stamp, expected.Holder.Stamp
+ expected.Inventory[r.box] = entry
+ return nil
+ })
+}
+
+func (r *claimRestoreObservation) retry(before, after hostclaim.Observed) error {
+ return r.advance(before, after, func(expected *hostclaim.Claim) error {
+ if expected.Op.Kind != hostclaim.KindServing || expected.Holder == nil || after.Claim.Holder == nil {
+ return hostclaim.ErrConflict
+ }
+ holder := *expected.Holder
+ holder.WakeRetry = after.Claim.Holder.WakeRetry
+ expected.Holder = &holder
+ return nil
+ })
+}
+
+func (r *claimRestoreObservation) advance(before, after hostclaim.Observed, delta func(*hostclaim.Claim) error) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ if !sameClaimObservation(r.observed, before) || before.Claim == nil || after.Claim == nil || after.Absent || after.Host != before.Host || after.ETag == "" || after.ETag == before.ETag || after.Claim.Gen != before.Claim.Gen+1 {
+ return hostclaim.ErrConflict
+ }
+ expected := *before.Claim
+ expected.Gen++
+ if err := delta(&expected); err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(&expected, after.Claim) {
+ return hostclaim.ErrConflict
+ }
+ r.observed = after
+ return nil
+}
diff --git a/internal/host/claimrestore_observation_linux_test.go b/internal/host/claimrestore_observation_linux_test.go
new file mode 100644
index 000000000..e42640b47
--- /dev/null
+++ b/internal/host/claimrestore_observation_linux_test.go
@@ -0,0 +1,133 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostclaim"
+)
+
+func TestClaimRestoreOwnWriteMaterializes(t *testing.T) {
+ for _, foreign := range []string{"", "before-retag", "after-restore"} {
+ t.Run(foreign, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ d := plan9D4Landing(t)
+ c := d.controller("boot-1", "controller-1", 4101, "start-1", true)
+ proofs := 0
+ d.f.materialize = func(ctx context.Context, o hostclaim.Observed) error {
+ h := o.Claim.Holder
+ tuple := hostclaim.OriginTuple{Incarnation: h.Incarnation, Location: h.Location, Region: "ie", Generation: h.Generation}
+ proven, err := hostclaim.AdmittedOrigins(ctx, d.f.claims, o, []hostclaim.OriginTuple{tuple})
+ if err != nil {
+ return err
+ }
+ if !proven[tuple] {
+ t.Fatal("admission origin not proven")
+ }
+ proofs++
+ return nil
+ }
+ materialize := func(o hostclaim.Observed, s activator.ClaimStart) error {
+ if err := s.Reserve(t.Context(), 0); err != nil {
+ return err
+ }
+ return s.Materialize(t.Context())
+ }
+ first, err := d.start(t, c, plan9D4Probe, false, materialize)
+ if err != nil {
+ t.Fatal("first start", err)
+ }
+ writeForeign := func() {
+ d.step(t, "foreign inventory write", func(claim *hostclaim.Claim) error {
+ claim.Inventory["box-1"].Paths[recoveryHost+".env"] = claim.Holder.Stamp
+ return nil
+ })
+ }
+ if foreign == "before-retag" {
+ retire := c.wake.RetireArtifacts
+ c.wake.RetireArtifacts = func(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, generation string) error {
+ writeForeign()
+ return retire(ctx, o, in, generation)
+ }
+ }
+ second, err := d.start(t, c, plan9D4Probe, true, func(o hostclaim.Observed, s activator.ClaimStart) error {
+ after := d.f.read()
+ if foreign != "before-retag" && after.Claim.Gen != o.Claim.Gen+1 {
+ t.Fatalf("Restore did not perform exactly its retag CAS: %d -> %d", o.Claim.Gen, after.Claim.Gen)
+ }
+ h := o.Claim.Holder
+ _, stale := hostclaim.AdmittedOrigins(t.Context(), d.f.claims, o, []hostclaim.OriginTuple{{Incarnation: h.Incarnation, Location: h.Location, Region: "ie", Generation: h.Generation}})
+ if !errors.Is(stale, hostclaim.ErrConflict) {
+ t.Fatalf("Prepare's read was not stale after retirement: %v", stale)
+ }
+ if foreign == "after-restore" {
+ writeForeign()
+ }
+ return materialize(o, s)
+ })
+ if foreign != "" {
+ if !errors.Is(err, hostclaim.ErrConflict) {
+ t.Fatalf("foreign claim write: %v", err)
+ }
+ if proofs != 1 {
+ t.Fatal("foreign writer reached C8", proofs)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal("retiring cold start", err)
+ }
+ d.requireRetired(t, first, second, false)
+ if proofs != 2 {
+ t.Fatal("both starts must prove C8 origins", proofs)
+ }
+ })
+ }
+}
+
+func TestClaimRestoreWriteOnlyRetagsPair(t *testing.T) {
+ for _, foreign := range []string{"", "op", "holder", "session", "stamp", "admission", "other-path", "before-etag", "generation"} {
+ t.Run(foreign, func(t *testing.T) {
+ f := newRecoveryFixture(t)
+ c := f.controller("boot", "controller", 4101, "start")
+ before := c.admit(t, f)
+ current, err := servingWriterIdentity(before, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ after, err := hostclaim.Update(t.Context(), f.claims, before, servingInventoryTransition(current, "state/data.db", 0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ r := &claimRestoreObservation{observed: before, box: "box-1", dataDir: f.guard.DataDir, db: filepath.Join(f.guard.DataDir, "state", "data.db")}
+ switch foreign {
+ case "op":
+ after.Claim.Op.Exec = "foreign"
+ case "holder":
+ after.Claim.Holder.App = "foreign"
+ case "session":
+ after.Claim.Holder.Session++
+ case "stamp":
+ after.Claim.Holder.Stamp++
+ case "admission":
+ after.Claim.Holder.Admissions[0].Generation = "foreign"
+ case "other-path":
+ after.Claim.Inventory["box-1"].Paths["foreign"] = current.Stamp
+ case "before-etag":
+ before.ETag = "foreign"
+ case "generation":
+ after.Claim.Gen++
+ }
+ err = r.retag(before, after)
+ if (err == nil) != (foreign == "") {
+ t.Fatalf("foreign %q: %v", foreign, err)
+ }
+ })
+ }
+}
diff --git a/internal/host/claimretire_local.go b/internal/host/claimretire_local.go
index 3c1567cf7..e92852408 100644
--- a/internal/host/claimretire_local.go
+++ b/internal/host/claimretire_local.go
@@ -88,7 +88,11 @@ func (r claimArtifactRetirement) Retire(ctx context.Context, admitted hostclaim.
return nil
}
retirement := fmt.Sprintf("superseded-%d-%d", c.Holder.Session, stamp)
- return r.Local.RetireSuperseded(ctx, admitted.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, c.Op.ID, retirement, stamp, []string{database, marker}, check)
+ before, after, err := r.Local.RetireSuperseded(ctx, admitted.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, c.Op.ID, retirement, stamp, []string{database, marker}, check)
+ if err != nil {
+ return err
+ }
+ return in.observeClaimWrite(before, after)
}
// stoppedAt requires root's completed full stop of the writer session that ran
diff --git a/internal/host/claimservinghandover.go b/internal/host/claimservinghandover.go
index 35f663518..e5afeb85b 100644
--- a/internal/host/claimservinghandover.go
+++ b/internal/host/claimservinghandover.go
@@ -12,7 +12,7 @@ import (
)
func (h ClaimRootHTTP) servingHandover() hostbox.ServingHandover {
- return hostbox.ServingHandover{ColdHandover: hostbox.ColdHandover{Guard: h.Stop.Coordinator.Guard, Claims: h.Stop.Coordinator.Evidence, Registry: h.Reconcile.Registry, Inventory: h.Reconcile.Inventory, Launches: h.Launches, Roots: h.Roots, JournalDir: filepath.Join(h.Reconcile.Retirement.JournalDir, "serving"), VerifyReceipt: verifyRetirementReceipt}, Validate: h.validateServingHandover}
+ return hostbox.ServingHandover{ColdHandover: hostbox.ColdHandover{Guard: h.Stop.Coordinator.Guard, Claims: h.Stop.Coordinator.Evidence, Registry: h.Reconcile.Registry, Inventory: h.Reconcile.Inventory, Launches: h.Launches, Roots: h.Roots, JournalDir: filepath.Join(h.Reconcile.Retirement.JournalDir, "serving"), VerifyReceipt: verifyRetirementReceipt}, Validate: h.validateServingHandover, VerifyDirectory: h.verifyServingHandoverDirectory}
}
func (h ClaimRootHTTP) validateServingHandover(ctx context.Context, c *hostclaim.Claim, prior hostguard.FenceTarget, next hostbox.ColdSuccessor, in hostbox.StopInventory, installation hostguard.RootInstallation) error {
diff --git a/internal/host/claimservinghandover_reboot_linux.go b/internal/host/claimservinghandover_reboot_linux.go
new file mode 100644
index 000000000..527f2de6f
--- /dev/null
+++ b/internal/host/claimservinghandover_reboot_linux.go
@@ -0,0 +1,74 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+func (h ClaimRootHTTP) verifyServingHandoverDirectory(ctx context.Context, prior hostguard.FenceTarget, e hostbox.StopInventoryEntry, actual hostbox.Artifact) error {
+ if h.servingParks == nil || h.servingParks.authority == nil {
+ return hostbox.ErrChanged
+ }
+ authority, ok := h.servingParks.authority.(*servingParkRoot)
+ if !ok {
+ return hostbox.ErrChanged
+ }
+ l := authority.ledger
+ if l == nil {
+ return hostbox.ErrChanged
+ }
+ fs, ok := l.fs.(*servingParkFS)
+ if !ok {
+ return hostbox.ErrChanged
+ }
+ boot, err := fs.currentBoot()
+ if err != nil || prior.BootID == boot || !validServingParkBootID(prior.BootID) {
+ return hostbox.ErrChanged
+ }
+ slot, err := l.slot(servingParkAuthorization{Successor: servingArtifactIdentity{Host: prior.Host, Box: prior.Box}})
+ if err != nil {
+ return err
+ }
+ r, err := l.readHistorical(slot.Last)
+ if err != nil {
+ return err
+ }
+ s := r.Authorization.Successor
+ if !r.Complete || s.Host != prior.Host || s.Box != prior.Box || s.Incarnation != prior.Incarnation || s.Stamp > prior.Stamp || e.Kind != "path" || e.ManagedRoot != "data" || e.RelativePath != "instances/"+provision.InstanceID(prior.Host) || e.Inode != r.Source.Directory.Inode || actual.Ino != e.Inode || actual.Type != 0040000 || e.Type != actual.Type {
+ return hostbox.ErrChanged
+ }
+ if _, err := fs.readNamespace(); err != nil {
+ return err
+ }
+ root, n, err := fs.namespace()
+ if err != nil {
+ return err
+ }
+ root.Close()
+ parent, pid, err := fs.parent()
+ if err != nil {
+ return err
+ }
+ defer parent.Close()
+ if pid != n.Parent {
+ return hostbox.ErrChanged
+ }
+ source, err := servingParkOpenDir(parent, provision.InstanceID(prior.Host))
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+ id, err := fs.rebootDirectory(source, r.Source.Directory, r.Source.Persistent, pid)
+ if err != nil || id.Device != actual.Dev || id.Inode != actual.Ino {
+ return hostbox.ErrChanged
+ }
+ if err := fs.checkBoot(boot); err != nil {
+ return err
+ }
+ return ctx.Err()
+}
diff --git a/internal/host/claimservinghandover_reboot_linux_test.go b/internal/host/claimservinghandover_reboot_linux_test.go
new file mode 100644
index 000000000..f23c6df88
--- /dev/null
+++ b/internal/host/claimservinghandover_reboot_linux_test.go
@@ -0,0 +1,62 @@
+//go:build linux
+
+package host
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+func TestServingHandoverRebootDirectoryProof(t *testing.T) {
+ for _, fault := range []string{"", "same-boot", "foreign-host", "inode", "handle", "mount", "missing-record"} {
+ t.Run(fault, func(t *testing.T) {
+ f, r := servingRebootPreparationFixture(t, "complete")
+ if err := f.p.commitServingReboot(t.Context(), r.Authorization.ID); err != nil {
+ t.Fatal(err)
+ }
+ fs := f.p.ledger.fs.(*servingParkFS)
+ prior := hostguard.FenceTarget{Host: r.Authorization.Successor.Host, Box: r.Authorization.Successor.Box, Incarnation: r.Authorization.Successor.Incarnation, Stamp: r.Authorization.Successor.Stamp, BootID: r.Source.BootID}
+ e := hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + prior.Host, Device: r.Source.Directory.Device + 1, Inode: r.Source.Directory.Inode, Type: 0040000}
+ a := hostbox.Artifact{Dev: r.Source.Directory.Device, Ino: e.Inode, Type: e.Type}
+ switch fault {
+ case "same-boot":
+ prior.BootID = servingOtherBoot
+ case "foreign-host":
+ prior.Host = "foreign.example"
+ case "inode":
+ e.Inode++
+ case "handle":
+ original := fs.handle
+ fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
+ p, err := original(d)
+ if filepath.Base(d.Name()) == prior.Host {
+ p.Handle += "00"
+ }
+ return p, err
+ }
+ case "mount":
+ binding, err := servingParkReadBinding(f.disk, "namespace")
+ if err != nil {
+ t.Fatal(err)
+ }
+ binding.Parent.Mount++
+ if err := f.p.ledger.write("current-namespace.json", binding); err != nil {
+ t.Fatal(err)
+ }
+ case "missing-record":
+ if err := f.disk.Remove("retirement-" + r.Authorization.ID + ".json"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ h := servingTransportRoot(t, f)
+ err := h.verifyServingHandoverDirectory(t.Context(), prior, e, a)
+ if (err == nil) != (fault == "") {
+ t.Fatalf("fault %q: %v", fault, err)
+ }
+ })
+ }
+}
diff --git a/internal/host/claimservinghandover_reboot_other.go b/internal/host/claimservinghandover_reboot_other.go
new file mode 100644
index 000000000..2cdecdc1f
--- /dev/null
+++ b/internal/host/claimservinghandover_reboot_other.go
@@ -0,0 +1,13 @@
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+func (h ClaimRootHTTP) verifyServingHandoverDirectory(context.Context, hostguard.FenceTarget, hostbox.StopInventoryEntry, hostbox.Artifact) error {
+ return hostbox.ErrUnsupported
+}
diff --git a/internal/host/claimservingretire.go b/internal/host/claimservingretire.go
index 11d0d0f29..123179a42 100644
--- a/internal/host/claimservingretire.go
+++ b/internal/host/claimservingretire.go
@@ -335,7 +335,14 @@ func (r *claimServingArtifactRetirement) enrollPairLocked(ctx context.Context, a
if stamp == current.Stamp {
return transition(fresh.Claim)
}
- _, updateErr := hostclaim.Update(ctx, r.Claims, fresh, transition)
+ written, updateErr := hostclaim.Update(ctx, r.Claims, fresh, transition)
+ if updateErr == nil {
+ if err := in.observeClaimWrite(fresh, written); err != nil {
+ return err
+ }
+ } else if in.claimWrite != nil {
+ return updateErr
+ }
// An uncertain CAS is resolved only by a fresh, exact pair and the same
// writer. A refusal never permits a database or marker write.
after, same, _, tagged, readErr := r.snapshot(ctx, admitted, in)
diff --git a/internal/host/claimwakeassembly.go b/internal/host/claimwakeassembly.go
index 5bfc8b23d..947a4f4c6 100644
--- a/internal/host/claimwakeassembly.go
+++ b/internal/host/claimwakeassembly.go
@@ -27,6 +27,7 @@ import (
// ClaimLaunchInputs is an in-process adapter result, never a new persisted
// held-input format. The moves adapter retains its MoveInputs and manifest.
type ClaimLaunchInputs struct {
+ claimWrite func(hostclaim.Observed, hostclaim.Observed) error
Roles []string
Identity hostwrite.Identity
Baseline *genselect.Plan
@@ -412,6 +413,8 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource
return activator.ClaimStart{}, err
}
_, in := a.snapshot()
+ observation := &claimRestoreObservation{observed: o, box: writer.Box, dataDir: source.Guard.DataDir, db: in.DBPath}
+ in.claimWrite = observation.retag
selected = claimLeaseWork{Work: selected, Alive: a.ctx, Check: a.leaseErr}
restore := hostbox.ColdRestore{Reader: writer.Data, Bucket: writer.Bucket, Generation: o.Claim.Holder.Generation, Work: selected, Identity: in.Identity, RetireArtifacts: func(ctx context.Context, g string) error { return w.RetireArtifacts(ctx, o, in, g) }}
if o.Claim.Op.Kind == hostclaim.KindServing {
@@ -453,18 +456,22 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource
}
return restore.Install(call, in.DBPath, in.Baseline)
}
+ s.RetryClaimWrite = observation.retry
s.Materialize = func(ctx context.Context) error {
- // Restore certifies the admitted root and advances the claim ETag.
- // C8 proves origins against the fresh observation of this writer.
+ expected := observation.snapshot()
fresh, err := read(ctx, o.Host)
if err != nil {
return err
}
- if !hostclaim.SameWriterIdentity(o, fresh) || !a.advance(fresh, w.Continue) {
+ if !sameClaimObservation(expected, fresh) {
+ return hostclaim.ErrConflict
+ }
+ if !a.advance(expected, w.Continue) {
return hostbox.ErrChanged
}
- return w.Inputs.Materialize(ctx, fresh, in, selected)
+ return w.Inputs.Materialize(ctx, expected, in, selected)
}
+
s.Build = func() *exec.Cmd {
cmd := exec.Command(in.Executable, in.Args...)
cmd.Env = append([]string(nil), in.Env...)
diff --git a/internal/host/plan12_restore_linux_test.go b/internal/host/plan12_restore_linux_test.go
index 52fcf9faf..b8175a0f6 100644
--- a/internal/host/plan12_restore_linux_test.go
+++ b/internal/host/plan12_restore_linux_test.go
@@ -319,8 +319,25 @@ func TestPlan12RecoveryRestoreSourceDatabase(t *testing.T) {
}
return
}
- if err := launch.Restore(t.Context()); err != nil {
- t.Fatal(err)
+ restoreErr := launch.Restore(t.Context())
+ if name == "concurrent-update" {
+ if !errors.Is(restoreErr, hostclaim.ErrConflict) {
+ t.Fatal("stale admission accepted foreign journal write", restoreErr)
+ }
+ if f.read().Claim.Op.Journal["concurrent"] != "acknowledged" {
+ t.Fatal("concurrent journal write lost")
+ }
+ if _, err := os.Stat(db); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("foreign update reached database installation", err)
+ }
+ retired := filepath.Join(f.guard.DataDir, ".parked", admitted.Claim.Holder.Incarnation, o.Host, fmt.Sprintf("superseded-%d-%d", admitted.Claim.Holder.Session, stamp), "artifacts", dbRel)
+ if got, err := os.ReadFile(retired); err != nil || string(got) != "source database" {
+ t.Fatalf("foreign update lost retired predecessor: %q %v", got, err)
+ }
+ return
+ }
+ if restoreErr != nil {
+ t.Fatal(restoreErr)
}
if got, err := os.ReadFile(db + ".gen"); err != nil || string(got) != admitted.Claim.Holder.Generation+"\n" {
t.Fatal(string(got), err)
diff --git a/internal/host/plan9_d4_retire_linux_test.go b/internal/host/plan9_d4_retire_linux_test.go
index 8b138ace5..64d2bfee3 100644
--- a/internal/host/plan9_d4_retire_linux_test.go
+++ b/internal/host/plan9_d4_retire_linux_test.go
@@ -188,7 +188,7 @@ func (d *plan9D4) controller(boot, controller string, pid int, start string, bou
// lease, then Admit, Prepare and the production Restore closure, whose
// ColdRestore calls RetireArtifacts. The written database stands in for the
// started app's own writes.
-func (d *plan9D4) start(t *testing.T, c *recoveryController, p activator.StartPolicy, stop bool) (hostclaim.Observed, error) {
+func (d *plan9D4) start(t *testing.T, c *recoveryController, p activator.StartPolicy, stop bool, materialize ...func(hostclaim.Observed, activator.ClaimStart) error) (hostclaim.Observed, error) {
t.Helper()
ctx := t.Context()
o := d.f.read()
@@ -215,6 +215,11 @@ func (d *plan9D4) start(t *testing.T, c *recoveryController, p activator.StartPo
if err := s.Restore(ctx); err != nil {
return admitted, err
}
+ for _, run := range materialize {
+ if err := run(admitted, s); err != nil {
+ return admitted, err
+ }
+ }
session := admitted.Claim.Holder.Session
return admitted, os.WriteFile(d.db, []byte(fmt.Sprintf("written by session %d", session)), 0600)
}
diff --git a/internal/hostbox/servinghandover.go b/internal/hostbox/servinghandover.go
index 14fac53eb..5414a5e5a 100644
--- a/internal/hostbox/servinghandover.go
+++ b/internal/hostbox/servinghandover.go
@@ -18,8 +18,9 @@ import (
// barrier covers inventory and row transfer; database retirement follows later.
type ServingHandover struct {
ColdHandover
- Validate func(context.Context, *hostclaim.Claim, hostguard.FenceTarget, ColdSuccessor, StopInventory, hostguard.RootInstallation) error
- checkpoint func(string) error
+ Validate func(context.Context, *hostclaim.Claim, hostguard.FenceTarget, ColdSuccessor, StopInventory, hostguard.RootInstallation) error
+ VerifyDirectory func(context.Context, hostguard.FenceTarget, StopInventoryEntry, Artifact) error
+ checkpoint func(string) error
}
func (h ServingHandover) Run(ctx context.Context, prior hostguard.FenceTarget, next ColdSuccessor) error {
@@ -139,6 +140,13 @@ func (h ServingHandover) Run(ctx context.Context, prior hostguard.FenceTarget, n
rebound.InstallationStamp, rebound.DirectoryRevision = next.Stamp, next.DirectoryRevision
for i := range rebound.Entries {
e := &rebound.Entries[i]
+ if e.Kind == "path" {
+ a, err := h.directory(ctx, prior, *e)
+ if err != nil {
+ return err
+ }
+ e.Device = a.Dev
+ }
e.Ownership = j.After.Route.ClaimOwner
if e.Kind == "route" {
e.RowIdentity = j.After.Route.ClaimRowID
@@ -213,13 +221,9 @@ func (h ServingHandover) validate(ctx context.Context, j coldJournal, c *hostcla
if e.ManagedRoot != "data" || e.RelativePath != stateRelative || e.Type != 0040000 || e.Device == 0 || e.Inode == 0 {
return ErrTypedInventory
}
- a, err := (TypedParker{Roots: h.Roots, ParkedRoots: h.Roots, Incarnation: p.Incarnation, Host: p.Host, Retirement: j.ID}).CaptureArtifact(e.ManagedRoot, e.RelativePath, e.Ownership, ArtifactPath)
- if err != nil {
+ if _, err := h.directory(ctx, p, e); err != nil {
return err
}
- if a.Dev != e.Device || a.Ino != e.Inode || a.Type != e.Type {
- return ErrChanged
- }
case "timer":
timers++
found := 0
@@ -241,6 +245,25 @@ func (h ServingHandover) validate(ctx context.Context, j coldJournal, c *hostcla
return h.Validate(ctx, c, p, n, j.Inventory, *j.ServingInstallation)
}
+func (h ServingHandover) directory(ctx context.Context, prior hostguard.FenceTarget, e StopInventoryEntry) (Artifact, error) {
+ a, err := (TypedParker{Roots: h.Roots, ParkedRoots: h.Roots, Incarnation: prior.Incarnation, Host: prior.Host, Retirement: "serving-handover"}).CaptureArtifact(e.ManagedRoot, e.RelativePath, e.Ownership, ArtifactPath)
+ if err != nil {
+ return a, err
+ }
+ if a.Ino != e.Inode || a.Type != e.Type {
+ return a, ErrChanged
+ }
+ if a.Dev != e.Device {
+ if h.VerifyDirectory == nil {
+ return a, ErrChanged
+ }
+ if err := h.VerifyDirectory(ctx, prior, e, a); err != nil {
+ return a, err
+ }
+ }
+ return a, nil
+}
+
func (h ServingHandover) Recover(ctx context.Context) error {
entries, err := readTrustedJournalDir(h.JournalDir)
if err != nil {
diff --git a/internal/hostbox/servinghandover_linux_test.go b/internal/hostbox/servinghandover_linux_test.go
index 51df3f7ca..34a0b7008 100644
--- a/internal/hostbox/servinghandover_linux_test.go
+++ b/internal/hostbox/servinghandover_linux_test.go
@@ -5,6 +5,7 @@ package hostbox
import (
"context"
"errors"
+ "fmt"
"os"
"path/filepath"
"reflect"
@@ -17,7 +18,7 @@ import (
"github.com/carlosframework/platform/internal/registry"
)
-func servingHandoverFixture(t *testing.T) (ServingHandover, hostguard.FenceTarget, ColdSuccessor) {
+func servingHandoverFixture(t *testing.T, deviceDelta ...uint64) (ServingHandover, hostguard.FenceTarget, ColdSuccessor) {
t.Helper()
t.Setenv("CARLOS_HOST_CLAIM", "1")
source, store, initial := workFixture(t)
@@ -70,6 +71,9 @@ func servingHandoverFixture(t *testing.T) (ServingHandover, hostguard.FenceTarge
if err != nil {
t.Fatal(err)
}
+ if len(deviceDelta) > 0 {
+ a.Dev += deviceDelta[0]
+ }
in := StopInventory{Complete: true, Host: old.Host, Incarnation: holder.Incarnation, InstallationStamp: holder.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{
{Kind: "route", ArtifactID: "old-route", ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: "old-route", Ownership: owner, Materialization: "materialized"},
{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: rel, Ownership: owner, Materialization: "materialized", Device: a.Dev, Inode: a.Ino, Type: a.Type},
@@ -192,3 +196,51 @@ func TestServingHandoverStaleJournalDoesNotBlockHealthy(t *testing.T) {
t.Fatal("healthy journal not recovered", err)
}
}
+
+func TestServingHandoverRebindsOnlyProvenDirectory(t *testing.T) {
+ for _, proven := range []bool{false, true} {
+ t.Run(fmt.Sprint(proven), func(t *testing.T) {
+ h, prior, next := servingHandoverFixture(t, 1)
+ in, _, err := h.Inventory.Snapshot(t.Context(), prior.Host, hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp})
+ if err != nil {
+ t.Fatal(err)
+ }
+ var live uint64
+ for _, e := range in.Entries {
+ if e.Kind == "path" {
+ live = e.Device - 1
+ }
+ }
+ calls := 0
+ h.VerifyDirectory = func(_ context.Context, p hostguard.FenceTarget, e StopInventoryEntry, a Artifact) error {
+ calls++
+ if p != prior || a.Dev != live || e.Device != live+1 || !proven {
+ return ErrChanged
+ }
+ return nil
+ }
+ err = h.Run(t.Context(), prior, next)
+ if !proven {
+ if err == nil {
+ t.Fatal("unproven device accepted")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ out, err := h.Inventory.ReadInstallation(t.Context(), next.Host, next.Incarnation, next.Stamp)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range out.Entries {
+ if e.Kind == "path" && e.Device != live {
+ t.Fatal("successor kept stale device")
+ }
+ }
+ if calls < 2 {
+ t.Fatal("proof not rechecked", calls)
+ }
+ })
+ }
+}
diff --git a/internal/park/local.go b/internal/park/local.go
index e23332d06..4deb9f44d 100644
--- a/internal/park/local.go
+++ b/internal/park/local.go
@@ -100,23 +100,23 @@ func (l Local) RetireArtifacts(ctx context.Context, host string, captured hostgu
// successor records before it materializes them (§6.6 P6-D2), so its own
// successor can retire them in turn. No stamp moves and every other path and
// box is left as found. A replay of the same retirement resumes its journal.
-func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostguard.Captured, op, retirement string, stamp uint64, paths []string, check func(*hostclaim.Claim) error) error {
+func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostguard.Captured, op, retirement string, stamp uint64, paths []string, check func(*hostclaim.Claim) error) (before, after hostclaim.Observed, resultErr error) {
if !localSupported() {
- return ErrLocalUnsupported
+ return before, after, ErrLocalUnsupported
}
if ctx == nil || check == nil || l.Claims == nil || l.Box == "" || l.DataDir == "" || l.Guard.DataDir != l.DataDir || captured.Incarnation == "" || captured.Stamp == 0 ||
stamp == 0 || stamp >= captured.Stamp || !localComponent(op) || !localComponent(retirement) || len(paths) == 0 {
- return ErrLocalIdentity
+ return before, after, ErrLocalIdentity
}
sorted := append([]string(nil), paths...)
sort.Strings(sorted)
for i := range sorted {
if !localPath(sorted[i]) {
- return ErrLocalInventory
+ return before, after, ErrLocalInventory
}
for k := i + 1; k < len(sorted); k++ {
if sorted[k] == sorted[i] || strings.HasPrefix(sorted[k], sorted[i]+"/") {
- return ErrLocalInventory
+ return before, after, ErrLocalInventory
}
}
}
@@ -169,7 +169,8 @@ func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostg
if fresh.Absent {
return ErrLocalIdentity
}
- _, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error {
+ before = fresh
+ after, err = hostclaim.Update(ctx, l.Claims, fresh, func(c *hostclaim.Claim) error {
if err := selected(c); err != nil {
return err
}
@@ -192,9 +193,9 @@ func (l Local) RetireSuperseded(ctx context.Context, host string, captured hostg
}
err := l.Guard.With(ctx, host, captured, guarded, work)
if errors.Is(err, errLocalComplete) {
- return nil
+ return before, after, nil
}
- return err
+ return before, after, err
}
func (l Local) RetireInstallation(ctx context.Context, host string, captured hostguard.Captured, id, op string) error {
diff --git a/internal/provision/plan14_legacy_identity_test.go b/internal/provision/plan14_legacy_identity_test.go
index ce7fc9d9f..578aa44f4 100644
--- a/internal/provision/plan14_legacy_identity_test.go
+++ b/internal/provision/plan14_legacy_identity_test.go
@@ -616,12 +616,17 @@ func TestPlan14SeedOverlayPreservesLegacySeal(t *testing.T) {
if err := loadRootReadInventory(root+"/internal/provision/testdata/plan14-root-reads-seed.tsv", &inv, true); err != nil {
t.Fatal(err)
}
- if len(inv.Legacy) != before+4 {
- t.Fatal("FS-R6 and FS-R10 permit exactly four additional legacy fingerprints")
+ if len(inv.Legacy) != before+7 {
+ t.Fatal("FS-R6, FS-R10 and FS-R11 preserve exactly seven additional legacy fingerprints")
}
- if errs := checkRootReadLegacy(inv, "e8d252989435bc3ee6e9c0d097f4d111100d63d19aadc1e2f8ea69e132b3301b"); len(errs) != 0 {
+ if errs := checkRootReadLegacy(inv, "586fc489beaebe0f6fbce9f5199d0e80379f2bd00e3e3e716a273d6fe2e67d36"); len(errs) != 0 {
t.Fatal(errs)
}
+ for _, key := range []string{"internal/activator/servingretry.go:ServingRetries.Wrap", "internal/activator/servingretry.go:ServingRetries.settle", "internal/park/local.go:Local.RetireSuperseded"} {
+ if e := inv.Entries[key]; e.Class != "legacy" || e.Task != "accepted-legacy" || !strings.Contains(e.Basis, "census-ruling-FS-R11") {
+ t.Fatalf("own-CAS threading changed legacy authority %s: %+v", key, e)
+ }
+ }
// FS-R6 keeps these existing legacy classes; the seed cannot promote them.
for _, key := range []string{"internal/activator/activator.go:Service.sweepOnce", "internal/activator/servingretry.go:ServingRetries.Wrap", "internal/host/relocationflush.go:ReleaseFlushFiles.Flush"} {
if e := inv.Entries[key]; e.Class != "legacy" || e.Task != "accepted-legacy" || !strings.Contains(e.Basis, "census-ruling-FS-R6") {
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 588e106bb..6892f03ae 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -748,7 +748,7 @@ func TestPlan14RootReadCensus(t *testing.T) {
t.Fatal(err)
}
// FS-R6 preserves three legacy readers; FS-R10 re-digests the test-only S3 server. Other proposals remain unapproved.
- seal = "e8d252989435bc3ee6e9c0d097f4d111100d63d19aadc1e2f8ea69e132b3301b"
+ seal = "586fc489beaebe0f6fbce9f5199d0e80379f2bd00e3e3e716a273d6fe2e67d36"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 444f01960..8e9c8eafb 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -479,6 +479,7 @@ var plan9B8CensusInventory = []censusEntry{
{"internal/host/claimservingmaintenance_linux.go", "claimServingRoom", "key-site", "lit:instances", "ce0cb25c53adfd3dba7344a98049f44fc3f0b3e633b22ad37299dea81503e403"},
// rev3=PROPOSED-seed: local instances mount/state references; no deployment record writer. Root authority remains in the seed overlay.
{"internal/host/claimservingpark_linux.go", "servingParkFS.parent", "key-site", "lit:instances", "1d241a7d446bbbade6e460efd911863f8a5246b8ac1d47b2548034e3feb2f216"},
+ {"internal/host/claimservinghandover_reboot_linux.go", "ClaimRootHTTP.verifyServingHandoverDirectory", "key-site", "lit:instances", "19e8315f54850af3a5ddc04ac6e2e9d9035b2392c9eae7c22e05000d956b7e1e"},
{"internal/host/claimservingparksource_linux.go", "servingParkStateDirectory", "key-site", "lit:instances", "ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "10d09b5ab6d07d316437233c9a65bf0bcdfe1083db4d0769fa8303b255dfd0ac"},
{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "10a99ff4b355907831a904010b7019d480ce93f2f595e99e076d3c702d10ca2b"},
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index fb7ab6ef8..57ceceeb6 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -25,7 +25,7 @@ internal/activator/seed_exclusion.go SeedExclusion.Release authority B,C,D,F,J4
internal/activator/seed_exclusion.go Service.AcquireSeedExclusion authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release cd98d58320a590564a599bd379cf146b3ed1499032089bbfac3ddde2424619aa
internal/activator/seed_exclusion.go type SeedExclusion authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release 9259b90bd5130f5abd76c46ffc395b89820e26c3bd73461128d82f4fedca96d6
internal/activator/servingretry.go ServingRetries.SettleAbandoned authority C,J4 rev3=PROPOSED-seed; current serving retry identity, stop settlement and bounded reservation; capacity checked before side effects and immediately before reservation; explicit rerun settlement spends no new attempt file:Exec@279,file:Read@387,file:Read@732 8885d486d4daea05411f8c5ab0afa5b340542bb624bbfe82518d0b1f5063d171
-internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; BeforeReserve refuses only; cleanup deadline bounds lifetime; route identity remains legacy file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@5991,file:Read@6656,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6522,whole:Route@876,whole:Route@954 e0fe1c8acbe4b1631ff9c3ce63dceba8297fbeab55fc6e4ace72be1d2d9d5bfa
+internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11;census-ruling-FS-R6; agent; existing route/input authority remains legacy; reports exact successful retry CAS to the runtime, which refuses unrelated changes file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@6084,file:Read@6768,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6634,whole:Route@876,whole:Route@954 e6ba56e2fc95197e5b765e2aa7e78d8366a351a5cb98e4720d000bb9975c3a34
internal/activator/swaphold.go Service.wakeFailureEvent diagnostic C,J4 rev3=PROPOSED-seed; incarnation-keyed missing-files diagnostics and indexed retry results; throttles output only and grants no retry, stop or launch debf64b1e73210eb4a6c88dd82f76e8be2e3e0a4f43e272341efe227703f9ca0
internal/host/agent.go runAgent authority B,C,D,F,J4 rev3=PROPOSED-seed; FS-R7/8/9; agent; signal subscription, cleanup budget and seed join share one activation gate; inactive lifecycle retains main startup joins, fresh 60-second service shutdown and fresh claims-close budget file:Exec@13674,file:Open@731,registry:App@10065,whole:Route@10057,whole:Route@22981 36e63cdf0cc75b4fae905d13f78cd13c14963753282f5767980d2a022b910505
internal/host/claimassembly.go ClaimAssembly.Build authority C,G,J4 rev3=PROPOSED-seed; captured claim source, writer and runtime resources; construct fresh private serving owner without copying mutex-bearing live state file:Prepare@3640,file:Prepare@3657,file:Prepare@4369,file:Read@4299 42fc9bdc95130d62b0c5001821a924499dbe8cbf0d7e72d78e2f367978d0c8a6
@@ -112,7 +112,7 @@ internal/host/claimservingdisplaced_linux.go const servingParkRootSearchLimit au
internal/host/claimservingdisplaced_linux.go servingParkFS.displacedRoot authority C,J4 rev3=PROPOSED-seed; completed private quarantine and prior namespace identity; no-follow bounded parent listing/statx and exact root-owned directory open; same device/inode/mount and safe parent recheck; no unknown child traversal file:Fstat@649,file:Read@144,file:Readdirnames@939,file:Statx@1259 762661ee74aaed30ca1147160a995786ad3a55200f6ffd25106125799ff0c74d
internal/host/claimservingdisplaced_linux.go servingParkFS.eventRoot authority C,J4 rev3=PROPOSED-seed; current namespace and recorded retirement root; selects only current or independently recorded prior namespace; ambiguous old absent-event records refuse file:Read@562 7c47b53f88059eea930d483465570fc7205b3cec27a118472071ca4d6df7d5e7
internal/host/claimservingdisplaced_linux.go servingParkFS.recordEventRoot authority C,J4 rev3=PROPOSED-seed; descriptor identity of an already verified event root; persist old-record namespace before first deletion; existing namespace must match exactly b4febb444cd09cd5ed45fc616d172966038d71026024c89dfce9e8c530435756
-internal/host/claimservinghandover.go ClaimRootHTTP.servingHandover authority C,J4 rev3=PROPOSED-seed; root-only serving transfer constructor and existing private journal, inventory and registry dependencies 213a1885dd9aa5d0079cd52c4f88c6893ea5b43bfbfd83a67ec2c4697a83265a
+internal/host/claimservinghandover.go ClaimRootHTTP.servingHandover authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; transfer constructor wires persistent directory proof for cross-boot device renumber 5cbb20b24e63783e1e95e3a468881280b7cd739063038bbc2d49392be2846ec9
internal/host/claimservinghandover.go ClaimRootHTTP.servingLegacyHandoverTarget authority C,J4 rev3=PROPOSED-seed; root-private no-process record selected by host/box/incarnation; only its retained exact stop tuple supplies legacy transfer authority file:Read@524 3fbec96fa13bd112c83253221e808a80943118b1dc06b6b5bcbbeb41ac5771d9
internal/host/claimservinghandover.go ClaimRootHTTP.validateServingHandover authority C,J4 rev3=PROPOSED-seed; fresh actor/census, claim, canonical deployment record, root installation and predecessor admission; exact retained session-zero no-process evidence or stopped serving predecessor file:Read@2660,file:Read@3770,registry:ClaimIncarnation@1500,registry:ClaimOwner@1642,registry:ClaimRowID@1664,registry:ClaimStamp@1528,whole:Route@1767 10a3ea792139e8328eec6292e9612d0f3d84c2a8500f8959ede3b92230a00788
internal/host/claimservinghandover.go servingPriorRevision authority C,J4 rev3=PROPOSED-seed; canonical recorded directory revision bounded by the current authenticated enrollment revision; never rewrites retained stop evidence 8007e188e2ad600cf36d8eed0fb5adb591f08f52be52285c85db348e0780cb84
@@ -261,7 +261,7 @@ internal/host/claimservingretire.go claimServingArtifactRetirement.BeforeLegacyI
internal/host/claimservingretire.go claimServingArtifactRetirement.EnrollLocked authority C,J4 rev3=PROPOSED-seed; completed root authorization cached for exact writer, fresh current tags and source absence; missing or stale permission refuses local mutation cf33600f5c3faaa0cde5658e53b1657ceadb7340244a68c9791441834a372729
internal/host/claimservingretire.go claimServingArtifactRetirement.Recover authority C,J4 rev3=PROPOSED-seed; admitted serving observation and launch inputs; resumes retirement without treating absent marker as enrollment authority 674a5a3c598cbdc29494a3cfc0859dd632c41be154a966ca7684f4440cbccdcb
internal/host/claimservingretire.go claimServingArtifactRetirement.Retire authority C,J4 rev3=PROPOSED-seed; captured predecessor generation; delegates only to serving recovery and root parking contract 6cf2e52f7e2aa511622268dd0a8dd37f1713e5beb4eba920e70057c2c90c32a3
-internal/host/claimservingretire.go claimServingArtifactRetirement.enrollPairLocked authority C,J4 rev3=PROPOSED-seed; fresh claim and root-bound source directory; descriptor-relative absence before and after paired tag CAS; exact readback resolves uncertain CAS 351faf39946ade5f9166b79c0a177001c8b4287ee698ce37c2c1e8337b534c30
+internal/host/claimservingretire.go claimServingArtifactRetirement.enrollPairLocked authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; root-bound source and descriptor-relative absence before and after paired CAS; runtime gets exact successful CAS; uncertain CAS refuses without adopting reread 0ba52acb5348df53692d6b34ecd8df2b0d1fee513cea7b10201d20b75f55f6b7
internal/host/claimservingretire.go claimServingArtifactRetirement.finish authority C,J4 rev3=PROPOSED-seed; root authorization; guarded paired-tag update before root completion; in-memory final-install permission only after completion; agent mount anchored before root call and same device/inode bound across namespaces; root records unchanged 98b740f4463d8b287640cbd31204a94e6aeac01a4ff006afeac546f07ac11190
internal/host/claimservingretire.go claimServingArtifactRetirement.forgetCompletion authority C,J4 rev3=PROPOSED-seed; in-memory completed authorization for the selected host; failed replay must invalidate prior final-install permission 771a11b74e74ca98cbb51a1da72ecffea91801c2b622448a559eb1bdc6f7b3b5
internal/host/claimservingretire.go claimServingArtifactRetirement.recover authority C,J4 rev3=PROPOSED-seed; fresh paired tags and predecessor admission; stop evidence before path-free root request; existing root record required for current-tag replay; agent mount anchored before root call and same device/inode bound across namespaces; root records unchanged 7a501bc99cec3aca9591746cf254b2e16049f1ee154dde9d415e1649df468d85
@@ -281,7 +281,7 @@ internal/host/claimservingtopology_linux.go type servingParkTopology authority C
internal/host/claimstop.go ClaimStopCoordinator.witnessNoProcess authority C,G,J4 rev3=PROPOSED-seed; existing peer, claim, root inventory and positive kernel no-process checks; local adopted holder requires new retained-evidence binding; original observation is retained before receipt publication file:Exec@1845,file:Read@2541,file:Read@668 4a0df51297d2f6ed2e9c284ee11c26b73747bf84e1da5214d00f190eece5eb38
internal/host/claimstop.go type ClaimStopCoordinator authority C,G,J4 rev3=PROPOSED-seed; root-only coordinator dependency for retaining its own session-zero observation; nil preserves existing refusal for adopted legacy holders b7114aa7340cdba82fef42d62b784eae16da7acc48407ae6edba6453a4889f9a
internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 393fdc63baeed67092c572fb003e9ed224d1ce37c556060033dffdb4bfcedcce
-internal/host/claimwakeassembly.go ClaimWakeAssembly.Build authority C,G,J1,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt file:Command@10308,file:Prepare@7029,file:Read@1348,file:Read@1442,registry:Backing@2672,registry:Host@12922,registry:Host@3354,registry:Host@3957,registry:Host@4002,registry:Host@4709,registry:Host@5430,registry:Host@6477,registry:Host@7185,registry:Host@7858,registry:Host@9166,registry:Seed@2655,registry:Tunnel@2727,registry:Unit@2692,whole:Route@10446,whole:Route@14642,whole:Route@14714,whole:Route@2899,whole:Route@5830 2613ac52b918f0b9a08551ceb05a8f0151ced0d22b10a449a96fae76af863d62
+internal/host/claimwakeassembly.go ClaimWakeAssembly.Build authority C,G,J1,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; Restore and retry own-write receipts advance exact captured observation; Materialize compares fresh head without adopting it, then uses tracked observation for C8; foreign delta refuses file:Command@10448,file:Prepare@7029,file:Read@1348,file:Read@1442,registry:Backing@2672,registry:Host@13062,registry:Host@3354,registry:Host@3957,registry:Host@4002,registry:Host@4709,registry:Host@5430,registry:Host@6477,registry:Host@7185,registry:Host@7858,registry:Host@9320,registry:Seed@2655,registry:Tunnel@2727,registry:Unit@2692,whole:Route@10586,whole:Route@14782,whole:Route@14854,whole:Route@2899,whole:Route@5830 11c2f2eb68339daf15ec0d9d86be5880ba1ea7f9a253e8a69000d603a05c4339
internal/host/claimwakeassembly.go type ClaimWakeAssembly authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt c13a15027ae22cf7a030b9cdedf13de4ae0f093f64b3e4a35c0614446964c94c
internal/host/claimwakestop.go ClaimAssembly.collectCapturedStop authority C,G,J4 rev3=PROPOSED-seed; fresh claim and verified local stop identity; session-zero legacy holder may name only the local writer box, with no admissions; root independently proves and retains no-process evidence file:Exec@2994,file:Read@605 d4cd80663dfae5f9737a74e319d3423f4ef853e1843666b02b4cd2341e869d72
internal/host/movesfiles.go movesFiles.Establish authority C,H3 rev3=PROPOSED-seed; ordinary origin proof and single C8 commit; private seed checks exact admitted writer and stages shared snapshot; no armed live provider 172fe8888da88c249b7990bd9fb4bd2821bd402fb96ab52af13c58500f31d50f
@@ -299,10 +299,10 @@ internal/hostbox/restore.go ColdRestore.Install authority C,J2,J4 rev3=PROPOSED-
internal/hostbox/restore.go ColdRestore.InstallEmpty authority C rev3=PROPOSED-seed; existing empty-bootstrap proof and local marker; legacy callback outside Work and enrollment callback before purge or marker creation f8c0ea4eaf3fa692abe52dd3b0799c9239bcb0d761c69644cc4be78d51d74c04
internal/hostbox/restore.go loadPinned authority C,J2,J4 rev3=PROPOSED-seed; exact selected LTX versions and original trailer/pin proof; validated bytes normalized for Litestream while retaining original database checksum for final comparison file:ReadAll@662 1e2b22c7c501d31cd1f2cb3be0fe419fa7e25afde87c6ecb43ca85edb7dbc3a4
internal/hostbox/servinghandover.go ServingHandover.Recover authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 6b90bd31f43571b0203059a49909b80fd35d7fbd42d1d8fe63685c928c7de3e9
-internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshot, exact stopped predecessor, canonical nested row, no-follow state identity, successor launch absence and private journal; fenced inventory/row transfer with retained old evidence registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5480,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5551,registry:ClaimRowID@5725,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5690,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5480,registry:Route@5551,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5633,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5482,whole:ColdRows@5553,whole:ColdRows@5635,whole:ColdRows@6216,whole:ColdRows@6226,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5488,whole:Route@5559,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5641 01b9e603a5f2a2476017777a7d02fafc76424d491201d6757170d66997fa481a
+internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; original stopped predecessor evidence remains immutable; successful persistent directory proof stores current device in successor inventory registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5615,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5686,registry:ClaimRowID@5860,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5825,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5615,registry:Route@5686,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5768,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5617,whole:ColdRows@5688,whole:ColdRows@5770,whole:ColdRows@6351,whole:ColdRows@6361,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5623,whole:Route@5694,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5776 bc5399896f9509e846062aba6bbefcaff5f9b4712a400a9b3f2473966d680b3f
internal/hostbox/servinghandover.go ServingHandover.at authority C,J4 rev3=PROPOSED-seed; in-process interruption seam; nil in production 0178e5fcfb2cc194fb4f33f0f230b52849ddeb87a4b7c4e6cfb640e8b2f00b8a
-internal/hostbox/servinghandover.go ServingHandover.validate authority C,J4 rev3=PROPOSED-seed; fresh serving claim, unchanged root-recorded inventory, exact recorded state-directory inode/type, complete route/timer set and production authority callback file:Exec@258,registry:ClaimIncarnation@2164,registry:ClaimOwner@1379,registry:ClaimOwner@2238,registry:ClaimOwnership@2132,registry:ClaimRowID@2273,registry:ClaimStamp@2207,registry:Name@2118,registry:Registration@2310,registry:Route@1033,registry:Route@1379,registry:Timers@2064,registry:Timers@2507,whole:ClaimOwnership@2138,whole:ColdRows@1035,whole:ColdRows@1381,whole:ColdRows@2066,whole:ColdRows@2509,whole:Route@1042,whole:Route@1388,whole:Timer@2073,whole:Timer@2516 2985e74f1c0aed6a6470e4ee4f03e794e32a97806542a51045a21e76ba7bce03
-internal/hostbox/servinghandover.go type ServingHandover authority C,J4 rev3=PROPOSED-seed; root-only serving transfer dependencies and validation callback; source state remains in place d56e1f341efd969be8757c2a2440170192d30338095c28ee9cba337de7a81bd6
+internal/hostbox/servinghandover.go ServingHandover.validate authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; unchanged inventory and stopped predecessor plus directory inode/type and boot-aware device proof file:Exec@258,registry:ClaimIncarnation@1906,registry:ClaimOwner@1379,registry:ClaimOwner@1980,registry:ClaimOwnership@1874,registry:ClaimRowID@2015,registry:ClaimStamp@1949,registry:Name@1860,registry:Registration@2052,registry:Route@1033,registry:Route@1379,registry:Timers@1806,registry:Timers@2249,whole:ClaimOwnership@1880,whole:ColdRows@1035,whole:ColdRows@1381,whole:ColdRows@1808,whole:ColdRows@2251,whole:Route@1042,whole:Route@1388,whole:Timer@1815,whole:Timer@2258 5935f65fef167c9e5c2265fb7fe5f25148f6aaa25f90c46ae970de9ad8fd7c0d
+internal/hostbox/servinghandover.go type ServingHandover authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; adds fail-closed directory verification callback, used only on changed device d92c342005a7d2a240716c60bb56d55a742bb5a54757e45b642c3c5625b99eb8
internal/hostbox/servingpark.go LaunchJournal.RootSessionLaunches authority C,J4 rev3=PROPOSED-seed; root-local persisted launch records for exact host/incarnation/stamp/session/box/enrollment; refuses agent journal transport cbbb9378bc53ac499972bed5fb654e05856a9afd7256d55323a717140ffcba18
internal/hostbox/servingpark_service.go JournalService.ExecuteServingPark authority C,J4 rev3=PROPOSED-seed; exact journal request and authenticated enrollment; durable pending receipt before filesystem effects, completed reply after effects; uncertain outcomes retain request df93855480924b39cf49edbeedd136799a7e327860ee4dec7742796d3d428333
internal/hostbox/servingpark_service.go JournalService.prepareServingPark authority C,J4 rev3=PROPOSED-seed; SQLite peer, digest, reply and retention boundary; exact completed replay, bounded request age and clock-rollback rejection before callback; existing bounded receipt pruning file:QueryRowContext@1050,file:QueryRowContext@331 83f1bae59de484a9d1cae09ce8e359aa173634c091203ea3ce4f6e8639e20d5e
@@ -324,9 +324,9 @@ internal/hostguard/writerstop.go Guard.CompletedServingStops authority C rev3=PR
system.go runSystem authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, claim flags, service identity and root delegation capability before constructing agent-mode resources; one-shot skips Configure, Start, duties and listeners; close resources only after private runtime joins d1150dfb2baa129605aa219052e89ffc790069d56f4a4227c63ca9f40855a81b
system.go systemVerb authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, claim flags, service identity and root delegation capability before constructing agent-mode resources; one-shot skips Configure, Start, duties and listeners; close resources only after private runtime joins ee25513ed72f9b8338d95682c4c76b4ced26ec1f060528fc5804da9afbfbfd83
@coverage . bf3d70b6a2c5d769a3c580c9d5511231499b788d9c08a542895de8381e5a39c9
-@coverage internal/activator 41f0ad01ac54bae63f089ef8ec5bbfa9155bb76720d72b2d88563481de40fe5c
-@coverage internal/host d5fedd5c2c15db96502000e3573af7a0f4d055e1c37f91aafea730349d557c7f
-@coverage internal/hostbox db890260533bdace6ad9a7c23dc1538cb919962894f005fc7c5a59a18e969429
+@coverage internal/activator 9bc3f0f89cc3902c34917f1c6b2f86a778cec0bed499e5ea783fac9d93c95a15
+@coverage internal/host 0b8d526b4e8b1504dafba96903efca8946b2d0bdb8c3ce35bf3b94b9131736e3
+@coverage internal/hostbox 86337ee9d1f7a4bdd15e331cac712c55d39da4cc26a7c013573a7a9535fe3975
@coverage internal/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
internal/host/rootexecution_recovery.go recoverRootExecutionBeforeUpdate authority D rev3=PROPOSED-seed; extracted update recovery ordering; root instance-control identity; broker durable controller state then best-effort local park and retirement journals; retained park failures cannot gate update eb97b162b0d712dd2fe8971984353ebc10efe1142077a3c2531781dd969cbbf5
@@ -342,3 +342,22 @@ internal/host/claimfilesseed_shutdown.go joinAgentShutdown authority C,J4 rev3=P
internal/host/claimservingvolume_linux.go servingParkFS.checkReapVolume authority C,J4 rev3=PROPOSED-seed; FS-R7/9; root; reads root-private per-retirement boot binding, requires its nonempty serial to match immutable source and current kernel volume, then checks the binding boot 80198a64328bcaf332275e3527f8bf2ca2011931b0b51fbdd97a0629a412052e
internal/host/claimservingvolume_linux.go servingParkVolume authority C,J4 rev3=PROPOSED-seed; FS-R7/9; root; filesystem device number resolved through kernel sysfs only, never filesystem UUID or tenant payload d23bbf792ad2ae2b48a8bb72b8127ab36fe0d3eb577775eac877ff22e75c8f0b
internal/host/claimservingvolume_linux.go servingParkVolumeAt authority C,J4 rev3=PROPOSED-seed; FS-R7/9; root; resolves sysfs /dev/block major:minor and containing disk for partitions, reads device/serial, accepts only normalized EBS volume IDs; missing/non-EBS serial grants no destructive rebind file:EvalSymlinks@75,file:ReadFile@386,file:Stat@241 d338711821e05b649ddb9af4674cb300e8781d71c72601f18e84351da51aa3a9
+internal/activator/claimwake.go type ClaimStart authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent runtime; callback carries exact before/after observations of its own retry CAS; no new read authority 634eab9d7912ea083c298038516e960dd21654c7c67d791f450c0b9867327a73
+internal/activator/servingretry.go ServingRetries.settle legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing claim selection remains legacy; returns exact successful retry settlement CAS through optional callback file:Read@264,file:Read@786 4d51344dfa9560d7bf5f5569efd5e00f902beb840e26afef6c1c2504e6f22fea
+internal/host/claimrestore_observation.go ClaimLaunchInputs.observeClaimWrite authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; forwards only actual own-write observations; idempotent no-write results leave the captured observation unchanged 6fac505e2f3e8a956038277a7a1070b0d83bcd78f0581103c91b1e73b71336c1
+internal/host/claimrestore_observation.go claimRestoreObservation.advance authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; exact predecessor ETag and body, host and generation increment, then exact allowed delta; no foreign write adopted 87c20d159c11b7c29a26ac1c875cd81a10af02b16c323ff62a62ff5928bb6227
+internal/host/claimrestore_observation.go claimRestoreObservation.retag authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; allows only the database/marker pair retag and serving nested layout already written by Restore; preserves op, holder, admission and all other inventory 6776edca272f8f0ca0cd7e47027c2802ec3707ba690852f005e9a3698cf956e4
+internal/host/claimrestore_observation.go claimRestoreObservation.retry authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; allows only runtime-owned WakeRetry delta with exact predecessor and generation validation d0b4e208de23586003f07b7a69427c2b9c1a759184e701380b971a9a0252c24c
+internal/host/claimrestore_observation.go claimRestoreObservation.snapshot authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; mutex-protected captured claim observation, no bucket read 23c4202ad30db4a6cc8282fefb9984bb8be4d52458ae28696ea985f370462eb3
+internal/host/claimrestore_observation.go sameClaimObservation authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; exact ETag and complete claim comparison; same writer alone is insufficient 460d2a7e29552be9aa2d5a90a962b8f1fea6ba7b494239a4d35f43d4efb9462b
+internal/host/claimrestore_observation.go type claimRestoreObservation authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; tracks only Prepare and proven own CAS results for Materialize 60f66cb892d1a4a43b7168c07ce3049eac30c7a1c4a018af646c38350114dd55
+internal/host/claimretire_local.go claimArtifactRetirement.Retire authority C,G,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; existing guarded db retirement, reports its actual before/after CAS to the runtime file:Read@900 d724a762d208822fe5e3c19d7ee03df0f911e1e7e20c05597702b42e57581bcc
+internal/host/claimservinghandover_reboot_linux.go ClaimRootHTTP.verifyServingHandoverDirectory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; reads private completed retirement and slot, current boot and bound namespace; exact host/inode/type and persistent filesystem/handle against current authorized mount before rebinding device; no deletion authority 19e8315f54850af3a5ddc04ac6e2e9d9035b2392c9eae7c22e05000d956b7e1e
+internal/host/claimservinghandover_reboot_other.go ClaimRootHTTP.verifyServingHandoverDirectory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; refuses unsupported non-Linux proof without reads 0932178d6251804b979c84d0131cfbc9f8bec2aa73d88256e77eee9d1526e68a
+internal/host/claimwakeassembly.go type ClaimLaunchInputs authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; agent; private callback carries own Restore CAS receipt without granting claim authority 25fd2977645bf4382c54aedef246b915eb63768da3c3d48be862797013182fcf
+internal/hostbox/servinghandover.go ServingHandover.directory authority C,J4 rev3=PROPOSED-seed; FS-R11/run31; root; descriptor-relative capture requires exact inode/type; device mismatch additionally requires production root-private persistent proof a6e6c6faabb9f74c4229bafec2473a66769dc7010b15653d0e3bfa66112ca69e
+internal/park/local.go Local.RetireSuperseded legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing guarded retirement remains legacy; returns its actual CAS before/after observations rather than substituting a reread file:Read@2127 e2245dfcfebbf206ccfbd6be5dc937a03639614af2a085f4deeb186413ab7a02
+@legacy 79007308de276cec4b99a8c01183456d9a93733db3f3781dc51f08ea9385b101 internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11;census-ruling-FS-R6; agent; existing route/input authority remains legacy; reports exact successful retry CAS to the runtime, which refuses unrelated changes file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@6084,file:Read@6768,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6634,whole:Route@876,whole:Route@954 e6ba56e2fc95197e5b765e2aa7e78d8366a351a5cb98e4720d000bb9975c3a34
+@legacy 68f5b655f5909a4e2f24d1357fd19c86c60b0a89a43846940cb7f72158e90181 internal/activator/servingretry.go ServingRetries.settle legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing claim selection remains legacy; returns exact successful retry settlement CAS through optional callback file:Read@264,file:Read@786 4d51344dfa9560d7bf5f5569efd5e00f902beb840e26afef6c1c2504e6f22fea
+@legacy 3e3459bce8d3eb4bc884f7de2f615cb3802ce399163b1effb873ab72f7f1b30a internal/park/local.go Local.RetireSuperseded legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed; census-ruling-FS-R11; agent; existing guarded retirement remains legacy; returns its actual CAS before/after observations rather than substituting a reread file:Read@2127 e2245dfcfebbf206ccfbd6be5dc937a03639614af2a085f4deeb186413ab7a02
+@coverage internal/park d9b0c51dfd1238851aa6642ed330400fd69611a08aab38b9f9708994309f1bae