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>
26 files changed,
+779
−56
internal/activator/claimwake.go+11 −10internal/activator/servingretry.go+16 −4internal/activator/servingretry_test.go+47 −0internal/host/claimfilesseed_isolated_rootrunner_linux_test.go+3 −0internal/host/claimfilesseed_reboot_rootrunner_linux_test.go+30 −0internal/host/claimfilesseed_renumber_rootrunner_linux_test.go+104 −0internal/host/claimfilesseed_restore_linux_test.go+11 −1internal/host/claimfilesseed_twohost_rootrunner_linux_test.go+1 −1internal/host/claimrestore_observation.go+93 −0internal/host/claimrestore_observation_linux_test.go+133 −0internal/host/claimretire_local.go+5 −1internal/host/claimservinghandover.go+1 −1internal/host/claimservinghandover_reboot_linux.go+74 −0internal/host/claimservinghandover_reboot_linux_test.go+62 −0internal/host/claimservinghandover_reboot_other.go+13 −0internal/host/claimservingretire.go+8 −1internal/host/claimwakeassembly.go+11 −4internal/host/plan12_restore_linux_test.go+19 −2internal/host/plan9_d4_retire_linux_test.go+6 −1internal/hostbox/servinghandover.go+30 −7internal/hostbox/servinghandover_linux_test.go+53 −1internal/park/local.go+9 −8internal/provision/plan14_legacy_identity_test.go+8 −3internal/provision/plan14_root_reads_test.go+1 −1internal/provision/plan9_b8_census_test.go+1 −0internal/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), ¤t); 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 |