WIP files seed checkpoint 4 (not for merge): root generation authorization and successor recovery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+1058
−27
internal/host/claimservingpark_records.go+132 −15internal/host/claimservingparkroot_linux.go+286 −0internal/host/claimservingparkroot_linux_test.go+551 −0internal/host/claimservingparksource_linux.go+1 −1internal/hostguard/servingretirement.go+67 −0internal/provision/testdata/plan14-root-reads-seed.tsv+21 −11
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 360.940s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.119s
ok github.com/carlosframework/platform/cmd/moveprobe 92.197s
ok github.com/carlosframework/platform/cmd/zddprobe 0.710s
ok github.com/carlosframework/platform/internal/activator 235.487s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 2.339s
ok github.com/carlosframework/platform/internal/apperr 0.073s
ok github.com/carlosframework/platform/internal/applog 0.132s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.503s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.011s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.702s
ok github.com/carlosframework/platform/internal/changelog 0.300s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 368.958s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.788s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.004s
ok github.com/carlosframework/platform/internal/domains 2.106s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.237s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.124s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 129.215s
ok github.com/carlosframework/platform/internal/hostactors 0.577s
ok github.com/carlosframework/platform/internal/hostbox 2.207s
ok github.com/carlosframework/platform/internal/hostclaim 0.510s
ok github.com/carlosframework/platform/internal/hostevidence 0.005s
ok github.com/carlosframework/platform/internal/hostexec 4.844s
ok github.com/carlosframework/platform/internal/hostfence 0.029s
ok github.com/carlosframework/platform/internal/hostfiles 10.503s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.011s
ok github.com/carlosframework/platform/internal/hostops 1.476s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.467s
ok github.com/carlosframework/platform/internal/hostresolve 0.137s
ok github.com/carlosframework/platform/internal/hostwrite 0.003s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.815s
ok github.com/carlosframework/platform/internal/instanceexec 2.705s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/instancekey 0.016s
ok github.com/carlosframework/platform/internal/instanceupdates 0.006s
ok github.com/carlosframework/platform/internal/layout 1.447s
ok github.com/carlosframework/platform/internal/lease 9.452s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.006s
ok github.com/carlosframework/platform/internal/ledger 0.013s
ok github.com/carlosframework/platform/internal/mail 0.167s
ok github.com/carlosframework/platform/internal/maintenance 0.087s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.508s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.073s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.995s
ok github.com/carlosframework/platform/internal/moves/archive 0.760s
ok github.com/carlosframework/platform/internal/objstore 0.170s
ok github.com/carlosframework/platform/internal/ops 0.168s
ok github.com/carlosframework/platform/internal/park 0.212s
ok github.com/carlosframework/platform/internal/privatefile 0.005s
ok github.com/carlosframework/platform/internal/provision 12.778s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.009s
ok github.com/carlosframework/platform/internal/relay 7.238s
ok github.com/carlosframework/platform/internal/release 19.927s
ok github.com/carlosframework/platform/internal/rollout 3.694s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.009s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.281s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.590s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.225s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.238s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.005s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.560s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.024s
=== RUN TestT4StorageProbeMinio
2026/10/14 07:29:15 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE54D0425B0CD9, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
Intent
Human
<task-notification> <task-id>b54plz3ih</task-id> <tool-use-id>toolu_01JAj6xdmGr9oZbQenFFDbuk</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/6387394d-0b76-4de7-bbb4-d725fe24b08c/tasks/b54plz3ih.output</output-file> <status>completed</status> <summary>Background command "Wait for seed build run 4" completed (exit code 0)</summary> </task-notification>
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index dd09c056a..aa451e1c5 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -41,6 +41,7 @@ type servingParkSource struct { |
| } |
| type servingParkRecord struct { |
| + Continuations []servingParkContinuation `json:",omitempty"` |
| Version int |
| Authorization servingParkAuthorization |
| Source servingParkSource |
| @@ -52,14 +53,26 @@ type servingParkRecord struct { |
| Cause string |
| } |
| +type servingParkContinuation struct { |
| + Authorization servingParkAuthorization |
| + Receipts []hostguard.FenceReceipt |
| +} |
| + |
| +func (r servingParkRecord) activeAuthorization() servingParkAuthorization { |
| + if len(r.Continuations) != 0 { |
| + return r.Continuations[len(r.Continuations)-1].Authorization |
| + } |
| + return r.Authorization |
| +} |
| + |
| type servingLegacyConsumption struct { |
| Authorization servingParkAuthorization |
| Source servingParkSource |
| } |
| type servingParkSlot struct { |
| - Version int |
| - Box, Host, ID, Pending string |
| + Version int |
| + Box, Host, ID, Pending, Last string |
| } |
| type servingParkFilesystem interface { |
| @@ -200,6 +213,29 @@ func validateServingParkRecord(r servingParkRecord) error { |
| if r.Complete && r.Disposition != servingParkRetain && !r.Closed { |
| return hostbox.ErrChanged |
| } |
| + previous := a |
| + if len(r.Continuations) > 32 { |
| + return hostbox.ErrChanged |
| + } |
| + for _, continuation := range r.Continuations { |
| + next := continuation.Authorization |
| + want := previous |
| + want.Successor = next.Successor |
| + want.TagsFrom = next.TagsFrom |
| + identity := previous.Successor |
| + identity.Stamp, identity.Session, identity.Generation = next.Successor.Stamp, next.Successor.Session, next.Successor.Generation |
| + if next != want || next.Successor != identity || identity.Stamp <= previous.Successor.Stamp || identity.Session != identity.Stamp || identity.Generation == "" || identity.Generation == previous.Successor.Generation || (next.TagsFrom != previous.TagsFrom && next.TagsFrom != previous.Successor.Stamp && next.TagsFrom != next.Successor.Stamp) || len(continuation.Receipts) == 0 || !r.Parked { |
| + return hostbox.ErrChanged |
| + } |
| + for _, receipt := range continuation.Receipts { |
| + t := receipt.Target |
| + p := previous.Successor |
| + if t.Host != p.Host || t.Box != p.Box || t.Region != p.Region || t.Incarnation != p.Incarnation || t.Stamp != p.Stamp || t.Session != p.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || verifyRetirementReceipt(context.Background(), receipt) != nil { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + previous = next |
| + } |
| return nil |
| } |
| @@ -235,6 +271,12 @@ func (l *servingParkLedger) slot(a servingParkAuthorization) (servingParkSlot, e |
| return s, hostbox.ErrChanged |
| } |
| } |
| + if saved.Last != "" { |
| + r, err := l.read(saved.Last) |
| + if err != nil || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host { |
| + return s, hostbox.ErrChanged |
| + } |
| + } |
| return saved, nil |
| } |
| @@ -275,6 +317,11 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| if slot.Pending != "" && slot.Pending != a.ID { |
| return zero, hostbox.ErrChanged |
| } |
| + unlockRecord, err := l.state.Lock(ctx, "retirement-"+a.ID+".lock") |
| + if err != nil { |
| + return zero, err |
| + } |
| + defer unlockRecord() |
| r, err := l.read(a.ID) |
| if errors.Is(err, os.ErrNotExist) { |
| source, e := l.fs.Capture(a) |
| @@ -330,6 +377,7 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| } |
| if slot.Pending == "" { |
| slot.Pending = a.ID |
| + slot.Last = a.ID |
| if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil { |
| return zero, err |
| } |
| @@ -353,18 +401,8 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| return zero, hostbox.ErrChanged |
| } |
| if slot.ID != "" { |
| - old, e := l.read(slot.ID) |
| - if e != nil || !old.Complete { |
| - return zero, hostbox.ErrChanged |
| - } |
| - if !old.Closed { |
| - cause := "replacement" |
| - if old.Deleting { |
| - cause = old.Cause |
| - } |
| - if e = l.deleteLocked(ctx, &old, cause); e != nil { |
| - return zero, e |
| - } |
| + if e = l.replaceLocked(ctx, slot.ID); e != nil { |
| + return zero, e |
| } |
| } |
| slot.ID = a.ID |
| @@ -404,13 +442,23 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| // completeLocked follows a fresh root check of the successor's paired tags. |
| // Its caller holds namespace and host authority, never an agent Work lock. |
| func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a servingArtifactIdentity) error { |
| + unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Box, a.Host)+".lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| r, err := l.read(id) |
| if err != nil { |
| return err |
| } |
| - if r.Authorization.Successor != a || !r.Parked { |
| + if r.activeAuthorization().Successor != a || !r.Parked { |
| return hostbox.ErrChanged |
| } |
| + unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlockRecord() |
| slot, err := l.slot(r.Authorization) |
| if err != nil { |
| return err |
| @@ -444,6 +492,45 @@ func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a ser |
| return nil |
| } |
| +// continueLocked records permission from a later stopped successor without |
| +// changing the original retirement, source identities or retention deadline. |
| +// Root has already rechecked the original and every continuation stop. |
| +func (l *servingParkLedger) continueLocked(ctx context.Context, id string, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) { |
| + var zero servingParkRecord |
| + unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock") |
| + if err != nil { |
| + return zero, err |
| + } |
| + defer unlock() |
| + unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock") |
| + if err != nil { |
| + return zero, err |
| + } |
| + defer unlockRecord() |
| + r, err := l.read(id) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if r.activeAuthorization() == a { |
| + return r, nil |
| + } |
| + slot, err := l.slot(a) |
| + if err != nil || slot.Pending != id || r.Complete { |
| + return zero, hostbox.ErrChanged |
| + } |
| + if err = l.fs.CheckParked(&r); err != nil { |
| + return zero, err |
| + } |
| + r.Continuations = append(r.Continuations, servingParkContinuation{Authorization: a, Receipts: receipts}) |
| + if err = ctx.Err(); err != nil { |
| + return zero, err |
| + } |
| + if err = l.save(r); err != nil { |
| + return zero, err |
| + } |
| + return r, nil |
| +} |
| + |
| func (l *servingParkLedger) deleteLocked(ctx context.Context, r *servingParkRecord, cause string) error { |
| if r.Closed { |
| return nil |
| @@ -481,6 +568,16 @@ func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error { |
| if err != nil { |
| return err |
| } |
| + unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlockRecord() |
| if r.Closed { |
| return nil |
| } |
| @@ -489,3 +586,23 @@ func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error { |
| } |
| return l.deleteLocked(ctx, &r, "expiry") |
| } |
| + |
| +func (l *servingParkLedger) replaceLocked(ctx context.Context, id string) error { |
| + unlock, err := l.state.Lock(ctx, "retirement-"+id+".lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + old, err := l.read(id) |
| + if err != nil || !old.Complete { |
| + return hostbox.ErrChanged |
| + } |
| + if old.Closed { |
| + return nil |
| + } |
| + cause := "replacement" |
| + if old.Deleting { |
| + cause = old.Cause |
| + } |
| + return l.deleteLocked(ctx, &old, cause) |
| +} |
diff --git a/internal/host/claimservingparkroot_linux.go b/internal/host/claimservingparkroot_linux.go| new file mode 100644 |
| index 000000000..35dd848a5 |
| --- /dev/null |
| +++ b/internal/host/claimservingparkroot_linux.go |
| @@ -0,0 +1,286 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "net" |
| + "path/filepath" |
| + "sort" |
| + "strconv" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// servingParkRoot composes local root authority with the filesystem ledger. |
| +// Production construction remains disabled until legacy authorization, journal |
| +// transport and startup recovery are implemented. Requests supply identities |
| +// only; installation paths and receipts are read under root's host lock. |
| +type servingParkRoot struct { |
| + root ClaimRootHTTP |
| + ledger *servingParkLedger |
| +} |
| + |
| +func (p *servingParkRoot) withAuthority(ctx context.Context, conn net.Conn, current servingArtifactIdentity, fn func(hostclaim.Observed, hostguard.FenceView, hostbox.Actor, servingArtifactDirectory, uint64) error) error { |
| + if ctx == nil || conn == nil || p == nil || p.ledger == nil || p.root.Stop.Authenticate == nil || p.root.RequireNewWork == nil || p.root.InventoryActor == nil || p.root.Journals == nil || p.root.Journals.Inventory == nil || p.root.Journals.Inventory.Remote() || p.root.Stop.Coordinator.Guard.Root == nil || fn == nil { |
| + return hostbox.ErrUnsupported |
| + } |
| + data := p.root.Roots["data"] |
| + if !filepath.IsAbs(data) || data != p.root.Stop.Coordinator.Guard.DataDir { |
| + return hostbox.ErrChanged |
| + } |
| + return p.ledger.withNamespace(ctx, func() error { |
| + return p.root.Stop.Coordinator.Guard.WithServingRetirement(ctx, current.Host, hostguard.Captured{Incarnation: current.Incarnation, Stamp: current.Stamp}, func(o hostclaim.Observed, view hostguard.FenceView) error { |
| + // Reauthenticate after waiting for locks: an earlier connection check |
| + // cannot establish that the edge main process is still this caller. |
| + peer, err := p.root.Stop.Authenticate(ctx, conn) |
| + if err != nil { |
| + return err |
| + } |
| + if peer.UID == 0 || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" { |
| + return ErrClaimStopIdentity |
| + } |
| + if err = p.root.RequireNewWork(ctx); err != nil { |
| + return err |
| + } |
| + actor, err := p.root.InventoryActor(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + if !actor.Eligible || actor.Quarantined || actor.Enrollment == "" || actor.Revision == 0 { |
| + return hostbox.ErrChanged |
| + } |
| + actual, err := servingWriterIdentity(o, actor.Box, actor.Region) |
| + if err != nil || actual != current || o.ETag == "" { |
| + return hostbox.ErrChanged |
| + } |
| + in, revision, err := p.root.Journals.Inventory.Snapshot(ctx, current.Host, view.Captured) |
| + if err != nil { |
| + return err |
| + } |
| + dir, err := servingParkStateDirectory(data, o, view.Installation, in, revision, actor) |
| + if err != nil { |
| + return err |
| + } |
| + db, err := filepath.Rel(data, provision.NestedDBPath(data, current.Host)) |
| + if err != nil { |
| + return err |
| + } |
| + tags, err := servingInventoryPair(o.Claim.Inventory[actor.Box], filepath.ToSlash(db)) |
| + if err != nil || tags > current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + if err = ctx.Err(); err != nil { |
| + return err |
| + } |
| + fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, current.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if fresh.ETag != o.ETag || fresh.Absent { |
| + return hostbox.ErrChanged |
| + } |
| + return fn(o, view, actor, dir, tags) |
| + }) |
| + }) |
| +} |
| + |
| +func servingRootStops(ctx context.Context, view hostguard.FenceView, prior servingArtifactIdentity, actor hostbox.Actor) ([]hostguard.FenceReceipt, error) { |
| + if prior.Host != view.Host || prior.Incarnation != view.Installation.Incarnation || prior.Box != actor.Box || prior.Region != actor.Region || prior.Stamp == 0 || prior.Session != prior.Stamp { |
| + return nil, hostbox.ErrChanged |
| + } |
| + known := view.Captured == (hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp}) |
| + for _, retired := range view.Retired { |
| + known = known || retired == (hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp}) |
| + } |
| + if !known { |
| + return nil, hostguard.ErrNoFenceReceipt |
| + } |
| + var out []hostguard.FenceReceipt |
| + seen := make(map[string]bool) |
| + for _, receipt := range view.Receipts { |
| + t := receipt.Target |
| + if t.Host != prior.Host || t.Incarnation != prior.Incarnation || t.Stamp != prior.Stamp || t.Box != prior.Box || t.Op != "" || t.Phase != "" || t.ObligationID != "" { |
| + continue |
| + } |
| + if t.Session != prior.Session || t.Region != prior.Region || t.Enrollment != actor.Enrollment || t.DirectoryRevision != strconv.FormatUint(actor.Revision, 10) || t.FenceID == "" || seen[t.FenceID] || verifyRetirementReceipt(ctx, receipt) != nil { |
| + return nil, ErrClaimStopReceipt |
| + } |
| + seen[t.FenceID] = true |
| + out = append(out, receipt) |
| + } |
| + if len(out) == 0 { |
| + return nil, hostguard.ErrNoFenceReceipt |
| + } |
| + sort.Slice(out, func(i, j int) bool { return out[i].Target.FenceID < out[j].Target.FenceID }) |
| + return out, nil |
| +} |
| + |
| +func sameServingRootStops(a, b []hostguard.FenceReceipt) bool { |
| + if len(a) != len(b) { |
| + return false |
| + } |
| + for i := range a { |
| + if a[i].Target != b[i].Target || !a[i].EvidenceAt.Equal(b[i].EvidenceAt) || !bytes.Equal(a[i].Bytes, b[i].Bytes) { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn, current, requestedPrior servingArtifactIdentity) (servingParkAuthorization, error) { |
| + var out servingParkAuthorization |
| + err := p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error { |
| + var a servingParkAuthorization |
| + var saved *servingParkRecord |
| + var continuation []hostguard.FenceReceipt |
| + var next servingParkAuthorization |
| + slot, err := p.ledger.slot(servingParkAuthorization{Successor: current}) |
| + if err != nil { |
| + return err |
| + } |
| + id := slot.Pending |
| + if id == "" && tags == current.Stamp { |
| + id = slot.Last |
| + } |
| + if id != "" { |
| + r, err := p.ledger.read(id) |
| + if err != nil { |
| + return err |
| + } |
| + a, saved = r.Authorization, &r |
| + if a.Legacy || a.Directory != dir { |
| + return hostbox.ErrChanged |
| + } |
| + active := r.activeAuthorization() |
| + if err = validateServingRootHistory(ctx, o, view, actor, r); err != nil { |
| + return err |
| + } |
| + if requestedPrior != (servingArtifactIdentity{}) { |
| + prior, e := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation) |
| + if e != nil || prior != requestedPrior { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + if tags != active.TagsFrom && tags != active.Successor.Stamp && tags != current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + if active.Successor != current { |
| + if r.Complete || active.Successor.Stamp >= current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + prior, e := servingPredecessor(o.Claim, current, active.Successor.Stamp, active.Successor.Generation) |
| + if e != nil || prior != active.Successor { |
| + return hostbox.ErrChanged |
| + } |
| + continuation, err = servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + next = active |
| + next.Successor, next.TagsFrom = current, tags |
| + } |
| + } else { |
| + if requestedPrior == (servingArtifactIdentity{}) { |
| + return hostbox.ErrChanged |
| + } |
| + prior, err := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation) |
| + if err != nil || prior != requestedPrior { |
| + return hostbox.ErrChanged |
| + } |
| + a = servingParkAuthorization{TagsFrom: tags, Predecessor: prior, OriginalSuccessor: current, Successor: current, Directory: dir} |
| + a.ID = servingRetirementID(prior, current, false) |
| + } |
| + prior, err := servingPredecessor(o.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation) |
| + if err != nil || prior != a.Predecessor { |
| + return hostbox.ErrChanged |
| + } |
| + receipts, err := servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + if saved != nil && !sameServingRootStops(saved.Receipts, receipts) { |
| + return ErrClaimStopReceipt |
| + } |
| + r, err := p.ledger.parkLocked(ctx, a, receipts) |
| + if err != nil { |
| + return err |
| + } |
| + if err = p.ledger.fs.CheckParked(&r); err != nil { |
| + return err |
| + } |
| + if next.ID != "" { |
| + r, err = p.ledger.continueLocked(ctx, a.ID, next, continuation) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + out = r.activeAuthorization() |
| + return nil |
| + }) |
| + return out, err |
| +} |
| + |
| +func (p *servingParkRoot) completeServingDatabasePark(ctx context.Context, conn net.Conn, id string, current servingArtifactIdentity) error { |
| + return p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error { |
| + r, err := p.ledger.read(id) |
| + if err != nil { |
| + return err |
| + } |
| + a := r.activeAuthorization() |
| + if err = validateServingRootHistory(ctx, o, view, actor, r); err != nil { |
| + return err |
| + } |
| + if a.Legacy || a.Successor != current || a.Directory != dir || tags != current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + prior, err := servingPredecessor(o.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation) |
| + if err != nil || prior != a.Predecessor { |
| + return hostbox.ErrChanged |
| + } |
| + receipts, err := servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + if !sameServingRootStops(r.Receipts, receipts) { |
| + return ErrClaimStopReceipt |
| + } |
| + return p.ledger.completeLocked(ctx, id, current) |
| + }) |
| +} |
| + |
| +// Every recorded successor must still be a retained local admission; each |
| +// transition carries the root stop of the successor it superseded. |
| +func validateServingRootHistory(ctx context.Context, o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, r servingParkRecord) error { |
| + current, err := servingWriterIdentity(o, actor.Box, actor.Region) |
| + if err != nil { |
| + return err |
| + } |
| + previous := r.Authorization.Successor |
| + for _, continuation := range r.Continuations { |
| + prior, err := servingPredecessor(o.Claim, current, previous.Stamp, previous.Generation) |
| + if err != nil || prior != previous { |
| + return hostbox.ErrChanged |
| + } |
| + receipts, err := servingRootStops(ctx, view, previous, actor) |
| + if err != nil { |
| + return err |
| + } |
| + if !sameServingRootStops(receipts, continuation.Receipts) { |
| + return ErrClaimStopReceipt |
| + } |
| + previous = continuation.Authorization.Successor |
| + } |
| + if previous == current { |
| + return nil |
| + } |
| + prior, err := servingPredecessor(o.Claim, current, previous.Stamp, previous.Generation) |
| + if err != nil || prior != previous { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/claimservingparkroot_linux_test.go b/internal/host/claimservingparkroot_linux_test.go| new file mode 100644 |
| index 000000000..1ac5b5b0c |
| --- /dev/null |
| +++ b/internal/host/claimservingparkroot_linux_test.go |
| @@ -0,0 +1,551 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "net" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "strings" |
| + "sync" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// Disk persistence and lock exclusion are real. Root ownership, mount policy, |
| +// roster and authenticated process observations remain explicit test seams. |
| +type servingAuthorityDisk struct { |
| + *rollbackDisk |
| + mu sync.Mutex |
| + locks map[string]chan struct{} |
| + trace *[]string |
| + prefix string |
| +} |
| + |
| +func (s *servingAuthorityDisk) Lock(ctx context.Context, name string) (func(), error) { |
| + s.mu.Lock() |
| + if s.locks == nil { |
| + s.locks = make(map[string]chan struct{}) |
| + } |
| + lock := s.locks[name] |
| + if lock == nil { |
| + lock = make(chan struct{}, 1) |
| + s.locks[name] = lock |
| + } |
| + s.mu.Unlock() |
| + select { |
| + case lock <- struct{}{}: |
| + if s.trace != nil { |
| + *s.trace = append(*s.trace, s.prefix+name) |
| + } |
| + return func() { <-lock }, nil |
| + case <-ctx.Done(): |
| + return nil, ctx.Err() |
| + } |
| +} |
| + |
| +type servingRootFixture struct { |
| + t *testing.T |
| + f *recoveryFixture |
| + p *servingParkRoot |
| + disk *servingAuthorityDisk |
| + rootDisk *servingAuthorityDisk |
| + conn net.Conn |
| + actor hostbox.Actor |
| + current, prior servingArtifactIdentity |
| + admitted hostclaim.Observed |
| + receipt hostguard.FenceReceipt |
| + db string |
| + trace []string |
| +} |
| + |
| +func newServingRootFixture(t *testing.T, launched bool) *servingRootFixture { |
| + t.Helper() |
| + f, _, _, in := servingRetirementFixture(t) |
| + first := servingAdmit(t, f, &in, "G") |
| + disk := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, prefix: "park:"} |
| + roots := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, prefix: "host:"} |
| + f.guard.Root = hostguard.NewRootAuthority(roots) |
| + journals, err := hostbox.NewJournalService(f.journal) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx := &servingRootFixture{t: t, f: f, disk: disk, rootDisk: roots, db: in.DBPath, actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7, Eligible: true}} |
| + fx.prior, err = servingWriterIdentity(first, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fs := newServingParkFS(f.guard.DataDir, disk) |
| + fs.owner = uint32(os.Geteuid()) |
| + fs.mount = func(string) (bool, error) { return true, nil } |
| + fx.p = &servingParkRoot{ledger: &servingParkLedger{state: disk, fs: fs, journal: f.journal, now: func() time.Time { return f.now }}} |
| + fx.p.root = ClaimRootHTTP{Roots: map[string]string{"data": f.guard.DataDir}, Journals: journals, |
| + Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: f.guard, Evidence: f.claims}, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { |
| + return StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: "boot", ControllerID: "controller"}, nil |
| + }}, |
| + InventoryActor: func(context.Context) (hostbox.Actor, error) { return fx.actor, nil }, RequireNewWork: func(context.Context) error { return nil }} |
| + fx.install(first) |
| + fx.tags(fx.prior.Stamp) |
| + fx.receipt = servingStop(t, f, f.read()) |
| + target := fx.receipt.Target |
| + if err := f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if launched { |
| + res := hostbox.LaunchReservation{ID: "attempt:main", Role: "main", InvocationID: "attempt", Inputs: "digest", Target: target, Group: hostbox.LaunchGroup{RelativePath: "system.slice/carlos-app@shop.service"}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}} |
| + if err := f.journal.IntendUnit(t.Context(), res); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.journal.StartedUnit(t.Context(), res, hostbox.LaunchGroup{RelativePath: res.Group.RelativePath, Device: 1, Inode: 4, InvocationID: "unit"}, 99, "tenant-start"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.journal.SettleUnit(t.Context(), target, res.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + o := f.read() |
| + fx.admitted, err = hostclaim.Update(t.Context(), f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G2"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx.current, err = servingWriterIdentity(fx.admitted, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for suffix, body := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} { |
| + if err := os.WriteFile(fx.db+suffix, []byte(body), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + left, right := net.Pipe() |
| + fx.conn = left |
| + t.Cleanup(func() { left.Close(); right.Close() }) |
| + disk.trace, roots.trace = &fx.trace, &fx.trace |
| + return fx |
| +} |
| +func (f *servingRootFixture) install(o hostclaim.Observed) { |
| + f.t.Helper() |
| + h := o.Claim.Holder |
| + d, err := os.Open(filepath.Dir(f.db)) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + id, err := servingParkDirID(d) |
| + d.Close() |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + owner := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, f.actor.Box))) |
| + in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + provision.InstanceID(o.Host), ArtifactID: "state", Ownership: hex.EncodeToString(owner[:]), Materialization: "materialized", Device: id.Device, Inode: id.Inode, Type: unix.S_IFDIR}}} |
| + inventory := f.p.root.Journals.Inventory |
| + if err := inventory.WriteStopInventory(f.t.Context(), in); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + _, revision, err := inventory.Snapshot(f.t.Context(), o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + root := hostguard.RootInstallation{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Migration: 1, InventoryRevision: revision, Account: h.Account, App: h.App, Record: h.Key, Box: f.actor.Box, Enrollment: f.actor.Enrollment} |
| + if err := f.f.guard.EstablishRoot(f.t.Context(), root, func(*hostclaim.Claim) error { return nil }); err != nil { |
| + f.t.Fatal(err) |
| + } |
| +} |
| +func (f *servingRootFixture) tags(stamp uint64) { |
| + f.t.Helper() |
| + _, err := hostclaim.Update(f.t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { |
| + rel, _ := filepath.Rel(f.f.guard.DataDir, f.db) |
| + c.Inventory = map[string]hostclaim.InventoryEntry{f.actor.Box: {Layout: "nested", Paths: map[string]uint64{rel: stamp, rel + ".gen": stamp}}} |
| + return nil |
| + }) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| +} |
| +func (f *servingRootFixture) park() (servingParkAuthorization, error) { |
| + return f.p.parkServingDatabase(f.t.Context(), f.conn, f.current, f.prior) |
| +} |
| +func (f *servingRootFixture) mutateRoot(edit func(map[string]any)) { |
| + f.t.Helper() |
| + name := f.current.Host + ".json" |
| + b, err := f.rootDisk.Read(name) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + var m map[string]any |
| + if err = json.Unmarshal(b, &m); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + edit(m) |
| + b, err = json.Marshal(m) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + if err = f.rootDisk.Write(name, b); err != nil { |
| + f.t.Fatal(err) |
| + } |
| +} |
| +func TestServingParkRootGenerationAuthority(t *testing.T) { |
| + for _, launched := range []bool{false, true} { |
| + t.Run(fmt.Sprint(launched), func(t *testing.T) { |
| + f := newServingRootFixture(t, launched) |
| + a, err := f.park() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want := []string{"park:namespace.lock", "host:" + f.current.Host + ".lock", "park:slot-" + servingParkKey(f.current.Box, f.current.Host) + ".lock", "park:retirement-" + a.ID + ".lock"} |
| + if !reflect.DeepEqual(f.trace, want) { |
| + t.Fatalf("lock order: %v", f.trace) |
| + } |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err == nil { |
| + t.Fatal("completed before retag") |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err := f.p.ledger.read(a.ID) |
| + if err != nil || !r.Complete { |
| + t.Fatalf("record: %+v %v", r, err) |
| + } |
| + if (r.Disposition == servingParkRetain) != launched { |
| + t.Fatalf("wrong disposition: %v", r.Disposition) |
| + } |
| + again, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{}) |
| + if err != nil || again != a { |
| + t.Fatalf("lost completion reply: %+v %v", again, err) |
| + } |
| + if err = os.WriteFile(f.db, []byte("replacement"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{}); err == nil { |
| + t.Fatal("accepted replacement source") |
| + } |
| + }) |
| + } |
| +} |
| +func TestServingParkRootRefusesBeforeEffects(t *testing.T) { |
| + for _, attack := range []string{"peer", "peer-root", "census", "roster", "quarantine", "enrollment", "revision", "claim", "prior", "tags", "half-tags", "source", "inventory-revision", "receipt", "pending", "root-quarantine", "root-pending", "barrier", "canceled"} { |
| + t.Run(attack, func(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + ctx := t.Context() |
| + switch attack { |
| + case "peer": |
| + f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, ErrClaimStopIdentity } |
| + case "peer-root": |
| + f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { |
| + return StopPeer{PID: 77, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}, nil |
| + } |
| + case "census": |
| + f.p.root.RequireNewWork = func(context.Context) error { return hostbox.ErrChanged } |
| + case "roster": |
| + f.actor.Eligible = false |
| + case "quarantine": |
| + f.actor.Quarantined = true |
| + case "enrollment": |
| + f.actor.Enrollment = "other" |
| + case "revision": |
| + f.actor.Revision++ |
| + case "claim": |
| + f.current.Generation = "foreign" |
| + case "prior": |
| + f.prior.Generation = "foreign" |
| + case "tags": |
| + f.tags(f.current.Stamp) |
| + case "half-tags": |
| + _, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { |
| + rel, _ := filepath.Rel(f.f.guard.DataDir, f.db) |
| + delete(c.Inventory[f.actor.Box].Paths, rel+".gen") |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + case "source": |
| + if err := os.Rename(filepath.Dir(f.db), filepath.Dir(f.db)+".saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Dir(f.db), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "inventory-revision": |
| + in, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.prior.Host, hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = f.p.root.Journals.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "receipt": |
| + f.mutateRoot(func(m map[string]any) { |
| + s := m["State"].(map[string]any) |
| + s["fence_journal"].([]any)[0].(map[string]any)["bytes"] = "Zm9yZ2Vk" |
| + }) |
| + case "pending": |
| + f.mutateRoot(func(m map[string]any) { m["State"].(map[string]any)["fence_journal"] = []any{} }) |
| + case "root-quarantine": |
| + f.mutateRoot(func(m map[string]any) { m["Quarantined"] = true }) |
| + case "root-pending": |
| + f.mutateRoot(func(m map[string]any) { |
| + m["PendingInstallation"] = m["Installation"] |
| + m["PendingInventory"] = "cGVuZGluZw==" |
| + }) |
| + case "barrier": |
| + f.mutateRoot(func(m map[string]any) { m["Barrier"] = m["State"].(map[string]any)["active_fence"] }) |
| + case "canceled": |
| + var cancel context.CancelFunc |
| + ctx, cancel = context.WithCancel(ctx) |
| + cancel() |
| + } |
| + if _, err := f.p.parkServingDatabase(ctx, f.conn, f.current, f.prior); err == nil { |
| + t.Fatal("unproved authority accepted") |
| + } |
| + names, err := f.disk.Names() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(names) != 0 { |
| + t.Fatalf("refusal wrote private state: %v", names) |
| + } |
| + db := f.db |
| + if attack == "source" { |
| + db = filepath.Join(filepath.Dir(db)+".saved", filepath.Base(db)) |
| + } |
| + if b, err := os.ReadFile(db); err != nil || string(b) != "served bytes" { |
| + t.Fatalf("source changed: %q %v", b, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type servingRootClientFixture struct{ f *servingRootFixture } |
| + |
| +func (c servingRootClientFixture) Park(ctx context.Context, current, prior servingArtifactIdentity) (servingParkAuthorization, error) { |
| + return c.f.p.parkServingDatabase(ctx, c.f.conn, current, prior) |
| +} |
| +func (c servingRootClientFixture) ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) { |
| + return servingParkAuthorization{}, hostbox.ErrUnsupported |
| +} |
| +func (c servingRootClientFixture) Complete(ctx context.Context, id string, current servingArtifactIdentity) error { |
| + return c.f.p.completeServingDatabasePark(ctx, c.f.conn, id, current) |
| +} |
| + |
| +type servingRootWork struct { |
| + g hostguard.Guard |
| + o hostclaim.Observed |
| +} |
| + |
| +func (w servingRootWork) With(ctx context.Context, _ hostwrite.Identity, fn func() error) error { |
| + return w.g.With(ctx, w.o.Host, hostguard.Captured{Incarnation: w.o.Claim.Holder.Incarnation, Stamp: w.o.Claim.Holder.Stamp}, func(c *hostclaim.Claim) error { |
| + if !claimSameWriter(w.o, hostclaim.Observed{Host: w.o.Host, Claim: c}, nil) { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + }, fn) |
| +} |
| +func TestServingParkRootProductionRestoreSuccessor(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + plan := plan12RestoreBaseline(t, f.f) |
| + f.install(f.admitted) |
| + r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: servingRootClientFixture{f}} |
| + in := ClaimLaunchInputs{DBPath: f.db} |
| + work := servingRootWork{f.f.guard, f.admitted} |
| + restore := hostbox.ColdRestore{Reader: f.f.data, Bucket: f.f.bucket, Generation: f.current.Generation, Work: work, |
| + BeforeLegacyInstall: func(ctx context.Context, _, _ string) error { return r.BeforeLegacyInstall(ctx, f.admitted, in, work) }, |
| + BeforeInstallLocked: func(ctx context.Context, _, _ string) error { return r.EnrollLocked(ctx, f.admitted, in) }, |
| + RetireArtifacts: func(ctx context.Context, generation string) error { return r.Retire(ctx, f.admitted, in, generation) }} |
| + if err := r.Recover(t.Context(), f.admitted, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := restore.Install(t.Context(), f.db, plan); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := mwReadDB(t, f.db); got != "selected recovery baseline" { |
| + t.Fatal(got) |
| + } |
| + marker, err := hostbox.ReadGenerationMarker(f.db + ".gen") |
| + if err != nil || marker != "G2" { |
| + t.Fatalf("marker %q %v", marker, err) |
| + } |
| + names, err := f.disk.Names() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, name := range names { |
| + if strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") { |
| + rec, err := f.p.ledger.read(strings.TrimSuffix(strings.TrimPrefix(name, "retirement-"), ".json")) |
| + if err != nil || !rec.Complete { |
| + t.Fatalf("incomplete: %v", err) |
| + } |
| + b, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", rec.Authorization.ID, filepath.Base(f.db))) |
| + if err != nil || string(b) != "served bytes" { |
| + t.Fatalf("parked predecessor: %q %v", b, err) |
| + } |
| + } |
| + } |
| +} |
| +func TestServingParkRootWaitReauthenticates(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + unlock, err := f.disk.Lock(t.Context(), "namespace.lock") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond) |
| + defer cancel() |
| + if _, err = f.p.parkServingDatabase(ctx, f.conn, f.current, f.prior); !errors.Is(err, context.DeadlineExceeded) { |
| + t.Fatalf("lock bypass: %v", err) |
| + } |
| + unlock() |
| + f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, ErrClaimStopIdentity } |
| + if _, err = f.park(); !errors.Is(err, ErrClaimStopIdentity) { |
| + t.Fatalf("stale authentication: %v", err) |
| + } |
| +} |
| + |
| +func TestServingParkRootContinuesOriginalRetirement(t *testing.T) { |
| + for _, point := range []string{"rename", "parked", "retag", "lost-continuation"} { |
| + t.Run(point, func(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + original := f.current |
| + fs := f.p.ledger.fs.(*servingParkFS) |
| + rename := fs.rename |
| + if point == "rename" { |
| + once := false |
| + fs.rename = func(a int, b string, c int, d string, e uint) error { |
| + err := rename(a, b, c, d, e) |
| + if err == nil && !once { |
| + once = true |
| + return errors.New("lost rename reply") |
| + } |
| + return err |
| + } |
| + } |
| + a, err := f.park() |
| + if point == "rename" { |
| + if err == nil { |
| + t.Fatal("fault missed") |
| + } |
| + fs.rename = rename |
| + a = servingParkAuthorization{ID: servingRetirementID(f.prior, original, false)} |
| + } else if err != nil { |
| + t.Fatal(err) |
| + } |
| + before, err := f.p.ledger.read(a.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if point == "retag" || point == "lost-continuation" { |
| + f.tags(original.Stamp) |
| + } |
| + f.install(f.f.read()) |
| + servingStop(t, f.f, f.f.read()) |
| + o := f.f.read() |
| + later, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G3"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(later, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.f.now = f.f.now.Add(2 * time.Hour) |
| + requested := f.prior |
| + if point == "retag" || point == "lost-continuation" { |
| + requested = original |
| + } |
| + if point == "lost-continuation" { |
| + disk := &servingParkFaultStore{Store: f.disk, failAt: 1} |
| + f.p.ledger.state = disk |
| + if _, err = f.p.parkServingDatabase(t.Context(), f.conn, f.current, requested); err == nil { |
| + t.Fatal("lost continuation reply missed") |
| + } |
| + f.p.ledger.state = f.disk |
| + } |
| + continued, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, requested) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if continued.ID != a.ID || continued.OriginalSuccessor != original || continued.Successor != f.current || continued.Predecessor != f.prior { |
| + t.Fatalf("relabelled retirement: %+v", continued) |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := f.p.ledger.read(a.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if after.Authorization != before.Authorization || len(after.Continuations) != 1 || !after.Complete { |
| + t.Fatalf("history: %+v", after) |
| + } |
| + if !before.ParkedAt.IsZero() && (!before.ParkedAt.Equal(after.ParkedAt) || !before.ReapAfter.Equal(after.ReapAfter)) { |
| + t.Fatal("continuation reset retention") |
| + } |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, original); err == nil { |
| + t.Fatal("old successor completed continuation") |
| + } |
| + again, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{}) |
| + if err != nil || again != continued { |
| + t.Fatalf("replay: %+v %v", again, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkRootRechecksClaimAfterInputs(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + original := f.p.root.Stop.Authenticate |
| + f.p.root.Stop.Authenticate = func(ctx context.Context, conn net.Conn) (StopPeer, error) { |
| + _, err := hostclaim.Update(ctx, f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { c.Inventory = nil; return nil }) |
| + if err != nil { |
| + return StopPeer{}, err |
| + } |
| + return original(ctx, conn) |
| + } |
| + if _, err := f.park(); err == nil { |
| + t.Fatal("used claim captured before peer and inventory checks") |
| + } |
| + names, err := f.disk.Names() |
| + if err != nil || len(names) != 0 { |
| + t.Fatalf("effects: %v %v", names, err) |
| + } |
| +} |
| +func TestServingParkRootContinuationNeedsStop(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + a, err := f.park() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.install(f.admitted) |
| + original := f.current |
| + o := f.f.read() |
| + next, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G3"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(next, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.park(); err == nil { |
| + t.Fatal("continued without original successor stop") |
| + } |
| + rec, err := f.p.ledger.read(a.ID) |
| + if err != nil || rec.activeAuthorization().Successor != original || len(rec.Continuations) != 0 { |
| + t.Fatalf("changed record: %+v %v", rec, err) |
| + } |
| +} |
diff --git a/internal/host/claimservingparksource_linux.go b/internal/host/claimservingparksource_linux.go| index 3d8f89555..462d42c4c 100644 |
| --- a/internal/host/claimservingparksource_linux.go |
| +++ b/internal/host/claimservingparksource_linux.go |
| @@ -18,7 +18,7 @@ import ( |
| ) |
| // servingParkStateDirectory accepts only root installation and root inventory |
| -// snapshots. The future root handler must read both locally under host authority; |
| +// snapshots. The root handler reads both locally under host authority; |
| // this function is not a transport decoder and accepts no registry row. |
| func servingParkStateDirectory(data string, o hostclaim.Observed, installation hostguard.RootInstallation, in hostbox.StopInventory, revision uint64, actor hostbox.Actor) (servingArtifactDirectory, error) { |
| var zero servingArtifactDirectory |
diff --git a/internal/hostguard/servingretirement.go b/internal/hostguard/servingretirement.go| new file mode 100644 |
| index 000000000..7672d0ee3 |
| --- /dev/null |
| +++ b/internal/hostguard/servingretirement.go |
| @@ -0,0 +1,67 @@ |
| +package hostguard |
| + |
| +import ( |
| + "context" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| +) |
| + |
| +// WithServingRetirement holds both root and shared host locks. Unlike Work, |
| +// retirement may precede installation of the newly admitted stamp. The callback |
| +// must authenticate the older installation and stop receipts before effects. |
| +// It must not call a lock-taking Guard or RootAuthority method. |
| +func (g Guard) WithServingRetirement(ctx context.Context, host string, current Captured, fn func(hostclaim.Observed, FenceView) error) error { |
| + if ctx == nil || g.Root == nil || g.FenceView != nil || current.Incarnation == "" || current.Stamp == 0 || fn == nil { |
| + return ErrInvalidInput |
| + } |
| + return g.locked(ctx, host, func(d *recordDir) error { |
| + o, err := g.freshObserved(ctx, host) |
| + if err != nil { |
| + return err |
| + } |
| + c, h := o.Claim, o.Claim.Holder |
| + if h.Incarnation != current.Incarnation || h.Stamp != current.Stamp || h.Session != current.Stamp || c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" { |
| + return ErrStampMismatch |
| + } |
| + r, err := d.read(host) |
| + if err != nil { |
| + return err |
| + } |
| + if r.quarantined || r.pendingInstallation != nil || r.barrier != nil { |
| + return ErrFenced |
| + } |
| + if r.ActiveFence != nil { |
| + completed := false |
| + for _, receipt := range r.FenceJournal { |
| + completed = completed || receipt.Target == *r.ActiveFence |
| + } |
| + if !completed || r.ActiveFence.Incarnation != current.Incarnation || r.ActiveFence.Stamp >= current.Stamp { |
| + return ErrFenced |
| + } |
| + } |
| + if r.Incarnation != current.Incarnation || r.ActiveStamp == 0 || r.ActiveStamp > current.Stamp { |
| + return ErrStampMismatch |
| + } |
| + for _, retired := range r.rootRetired { |
| + if retired == current { |
| + return ErrRetiredStamp |
| + } |
| + } |
| + for _, receipt := range r.FenceJournal { |
| + if receipt.Target.matchesSession(c, current) { |
| + return ErrFenced |
| + } |
| + } |
| + view, err := g.Root.viewLocked(host, Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp}) |
| + if err != nil { |
| + return err |
| + } |
| + if err = view.validate(host, view.Captured); err != nil { |
| + return err |
| + } |
| + if err = ctx.Err(); err != nil { |
| + return err |
| + } |
| + return fn(o, view) |
| + }) |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index e5864bd32..70e5eb676 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-seed.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-seed.tsv |
| @@ -1,5 +1,5 @@ |
| -# Files seed checkpoint 3 overlay; pending Paul’s census ruling. |
| -# Root filesystem and retirement-ledger mechanisms have test callers only; production binding is refused. |
| +# Files seed checkpoint 4 overlay; pending Paul’s census ruling. |
| +# Root generation authorization and continuation have test callers only; production binding is refused. |
| # Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed. |
| # Every classification below is rev3=PROPOSED-seed, not an approved disposition. |
| internal/host/claimconstruction.go OpenProductionClaimAssembly authority C,G rev3=PROPOSED-seed; box process identity and provider bindings; serving dispatch independent of moves; serving enrollment required before a runtime can be built file:Read@4150,file:Read@9550,file:ReadFile@3694,registry:Host@9577,whole:Route@9665 088f34c7fe1b4f9bb7a3187d40d60bff0fd811ebf1e1888ce3d4781dc4de80d7 |
| @@ -54,18 +54,28 @@ internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=P |
| internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29 |
| internal/host/claimservingpark_records.go servingParkDecode authority C,J4 rev3=PROPOSED-seed; strict private-record JSON with unknown-field and trailing-value refusal 189c7944ea9f5d269d39ed6a869b6bd739e3033195f06cc1f2c9f23a23180184 |
| internal/host/claimservingpark_records.go servingParkKey authority C,J4 rev3=PROPOSED-seed; box/host identity used for stable per-host namespace and slot keys 67518302550380dbf9b741a73758e5c94307eff70ff8a09cc56763a8aa794d86 |
| -internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 246eb0689f2f36a968c60d9241f52d2f3cf7ebf76b025092e1658f1b7b495236 |
| +internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 00f9c10b8130c134a41dea74848aa1651ae3331c8440a81e1b3f59f54f6a7218 |
| internal/host/claimservingpark_records.go servingParkLedger.deleteLocked authority C,J4 rev3=PROPOSED-seed; complete retained record or proved discard disposition and persisted cause/progress before filesystem deletion 1545950c564d51d9c29b0d76daca1ddec5a159481698b11accd4dd7411157b37 |
| -internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3312,file:Read@1564 d248f233de019cf9e28cf89bbf1e41f8e9c23089556ca709d18a9b076a04c09c |
| +internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3247,file:Read@1693 c46ddfb8aaf8339eb6d5dc414724ead938338552477825efe6d1e6f677897b32 |
| internal/host/claimservingpark_records.go servingParkLedger.read authority C,J4 rev3=PROPOSED-seed; private retirement record by validated deterministic ID and strict record invariants file:Read@249 868c8c3f5682dc36738d6960d57f2f1fcc6512374c8f8e0790187bf69cf3de64 |
| -internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry 2352daae92630f8e33bd7c9bdd70f382de9da9be559de8522982eb73cf935c09 |
| +internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry aec4f50b45d1d955fac9814fe9e0d403f5f3f7f939c71f8f6b8ac15e9173b846 |
| internal/host/claimservingpark_records.go servingParkLedger.save authority C,J4 rev3=PROPOSED-seed; validated private retirement record before durable rootstate write b6f940b1e21cf1beb67638effea958d2b1119695494a562e22bbbe0469d8af49 |
| -internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes file:Read@179 5c0d897b1bc9f0733d1dad18a90ce27900e371aaae2940e49cded65888d1c084 |
| -internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; production host-guard composition remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac |
| +internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes; last-retirement identity supports discarded/artifact-free completion replay file:Read@179 239b806b2e2bfdd568f5d6b749d299f0bc7b118d675e872429432591a89b7b68 |
| +internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; composed with root host authority in generation tests; production remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac |
| internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571 |
| internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7 |
| -internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants 0ab78151f2243224751d32fb309db9e36eed27487935c97f9531a72adf42fc62 |
| -internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; snapshot acquisition remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35 |
| -@coverage internal/host cc1a23e90ccf77b923b6c02649b3823cfbb0454368c06571e8e5292315becfc6 |
| +internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants; bounded immutable continuation history and exact predecessor stops db48025925e4b5acaf3231fdbf4d19900f406017b402efdf5ba1cb2550d00bc6 |
| +internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; root acquisition under host lock is exercised by generation tests; production remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35 |
| +internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108 |
| +internal/host/claimservingparkroot_linux.go servingParkRoot.withAuthority authority C,J4 rev3=PROPOSED-seed; independent active-edge peer authentication, census gate, roster, local root inventory and fresh claim recheck under namespace and root host authority; no registry path or caller receipt file:Read@2432 6d0c84a5a9c5403f166db4f200504a3ccd75ebcd7ddac1736e3ac046237a2882 |
| +internal/host/claimservingparkroot_linux.go servingRootStops authority C,J4 rev3=PROPOSED-seed; root fence snapshot and known installation stamps; exact local serving receipt bytes, enrollment, revision and unique fence identities d16eb077adfb8f0f8c9e63d1609250dc3470ec424bdde63b57c47d09500b2bce |
| +internal/host/claimservingparkroot_linux.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-exact comparison of persisted and freshly selected root receipt sets, including every target field and evidence time c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88 |
| +internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired claim tags, root slot and retirement records, retained predecessor and successor admissions, root stop history and source identity before park or continuation 802f4b8f5a2e49c3d6d4b0530c8cc484d29abf0a25ae852d1e7f39509d0e986e |
| +internal/host/claimservingparkroot_linux.go servingParkRoot.completeServingDatabasePark authority C,J4 rev3=PROPOSED-seed; fresh paired enrollment, same current writer, root source identity and complete original/continuation stop history before durable completion f120d235529b61ce93167f4c364a962b7dc4197ba0611719547015182cba96e1 |
| +internal/host/claimservingparkroot_linux.go validateServingRootHistory authority C,J4 rev3=PROPOSED-seed; retained local successor admissions and byte-exact root stops for every persisted continuation; rejects relabelled or missing history c85a1be96de5fa61acd48c4adf9393d8e837880f5133a2f965f1104037e02329 |
| +internal/host/claimservingpark_records.go servingParkRecord.activeAuthorization authority C,J4 rev3=PROPOSED-seed; last persisted continuation authorization or immutable original authorization; no source or deadline changes fe168fe52317e45ce70c3e97a9bc2c52d2409328ab4844a8d2a34fc4babba36d |
| +internal/host/claimservingpark_records.go servingParkLedger.continueLocked authority C,J4 rev3=PROPOSED-seed; namespace/host-protected slot and record locks; pending original retirement, absent source and retained parked identities before durable bounded continuation 87f297a924f7f153624304233218d8b5548a016fc7dbf0f7cb8dedb789324009 |
| +internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad |
| +@coverage internal/host 9900e8ad9fe9d49c4359c0d7d11ff5a804a5b7b54328a50bcf6c3dfde91f771e |
| @coverage internal/hostbox 72c9b640d98c779a044579f70a103a68cd9819c794a0bb3e53f378d39ad11419 |
| -@coverage internal/hostguard 51408b42f01be94a232e395494e5d154cc57736e32a6519ac6c03e9ff3d091e6 |
| +@coverage internal/hostguard 94912485e1e5fad4bad56985dd48ede776cfcb5fe18e0f4a59953584ddab06c0 |