WIP files seed checkpoint 21 (not for merge)
Persistent artifact evidence and reboot inspection checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 files changed,
+931
−61
internal/host/claimservingboot_linux.go+0 −11internal/host/claimservingidentity.go+97 −0internal/host/claimservingidentity_linux.go+53 −0internal/host/claimservinglegacy.go+1 −1internal/host/claimservinglegacyroot_linux.go+2 −2internal/host/claimservingpark_linux.go+42 −2internal/host/claimservingpark_records.go+24 −10internal/host/claimservingpark_rootrunner_linux_test.go+55 −3internal/host/claimservingreboot_linux.go+198 −0internal/host/claimservingreboot_linux_test.go+429 −0internal/host/claimservingtopology_linux.go+0 −16internal/provision/testdata/plan14-root-reads-seed.tsv+30 −16
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 356.191s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.169s
ok github.com/carlosframework/platform/cmd/moveprobe 92.197s
ok github.com/carlosframework/platform/cmd/zddprobe 0.706s
ok github.com/carlosframework/platform/internal/activator 234.118s
ok github.com/carlosframework/platform/internal/agentsock 0.026s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.021s
ok github.com/carlosframework/platform/internal/appconfig 2.279s
ok github.com/carlosframework/platform/internal/apperr 0.074s
ok github.com/carlosframework/platform/internal/applog 0.141s
ok github.com/carlosframework/platform/internal/awsinv 0.005s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.689s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.013s
ok github.com/carlosframework/platform/internal/caps 0.017s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.696s
ok github.com/carlosframework/platform/internal/changelog 0.291s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 368.883s
? 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.896s
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.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.117s
ok github.com/carlosframework/platform/internal/doorkey 0.012s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 26.076s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.128s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.017s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.032s
ok github.com/carlosframework/platform/internal/host 149.080s
ok github.com/carlosframework/platform/internal/hostactors 0.331s
ok github.com/carlosframework/platform/internal/hostbox 2.297s
ok github.com/carlosframework/platform/internal/hostclaim 0.515s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.776s
ok github.com/carlosframework/platform/internal/hostfence 0.035s
ok github.com/carlosframework/platform/internal/hostfiles 10.461s
ok github.com/carlosframework/platform/internal/hostfloor 0.014s
ok github.com/carlosframework/platform/internal/hostguard 1.010s
ok github.com/carlosframework/platform/internal/hostops 1.436s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.013s
ok github.com/carlosframework/platform/internal/hostquarantine 0.014s
ok github.com/carlosframework/platform/internal/hostrelocate 0.443s
ok github.com/carlosframework/platform/internal/hostresolve 0.132s
ok github.com/carlosframework/platform/internal/hostwrite 0.014s
ok github.com/carlosframework/platform/internal/infraview 0.014s
ok github.com/carlosframework/platform/internal/instancectl 1.811s
ok github.com/carlosframework/platform/internal/instanceexec 2.707s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.020s
ok github.com/carlosframework/platform/internal/layout 1.425s
ok github.com/carlosframework/platform/internal/lease 9.454s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.019s
ok github.com/carlosframework/platform/internal/ledger 0.030s
ok github.com/carlosframework/platform/internal/mail 0.163s
ok github.com/carlosframework/platform/internal/maintenance 0.076s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.494s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.073s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 21.027s
ok github.com/carlosframework/platform/internal/moves/archive 0.771s
ok github.com/carlosframework/platform/internal/objstore 0.187s
ok github.com/carlosframework/platform/internal/ops 0.171s
ok github.com/carlosframework/platform/internal/park 0.205s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 10.950s
ok github.com/carlosframework/platform/internal/publicrelease 0.010s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.039s
ok github.com/carlosframework/platform/internal/relay 7.231s
ok github.com/carlosframework/platform/internal/release 19.140s
ok github.com/carlosframework/platform/internal/rollout 3.634s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.008s
ok github.com/carlosframework/platform/internal/rootparticipation 0.006s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/s3compat 0.007s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.282s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.849s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.820s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.341s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.007s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.557s
=== 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.030s
=== RUN TestT4StorageProbeMinio
2026/10/14 21:35:07 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE82F8F0AD28CA, 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.028s
minio contract: PASS
diff --git a/internal/host/claimservingboot_linux.go b/internal/host/claimservingboot_linux.go| index 342c8154b..158d47629 100644 |
| --- a/internal/host/claimservingboot_linux.go |
| +++ b/internal/host/claimservingboot_linux.go |
| @@ -3,10 +3,8 @@ |
| package host |
| import ( |
| - "encoding/hex" |
| "io" |
| "os" |
| - "strings" |
| "github.com/carlosframework/platform/internal/hostbox" |
| ) |
| @@ -38,15 +36,6 @@ func (f *servingParkFS) currentBoot() (string, error) { |
| return id, nil |
| } |
| -func validServingParkBootID(id string) bool { |
| - if len(id) != 36 || id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' { |
| - return false |
| - } |
| - raw := strings.ReplaceAll(id, "-", "") |
| - b, err := hex.DecodeString(raw) |
| - return err == nil && len(b) == 16 && hex.EncodeToString(b) == raw && raw != strings.Repeat("0", 32) |
| -} |
| - |
| // A mount number is meaningful only in the boot that supplied it. Old records |
| // without a boot remain readable, but cannot authorize namespace mutations. |
| func (f *servingParkFS) checkBoot(expected string) error { |
diff --git a/internal/host/claimservingidentity.go b/internal/host/claimservingidentity.go| new file mode 100644 |
| index 000000000..02a05e675 |
| --- /dev/null |
| +++ b/internal/host/claimservingidentity.go |
| @@ -0,0 +1,97 @@ |
| +package host |
| + |
| +import ( |
| + "encoding/hex" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| +) |
| + |
| +// Handles identify an inode within its filesystem across boots. Device and |
| +// inode numbers remain required too; these handles never authorize an open. |
| +type servingParkPersistentDirectory struct { |
| + Filesystem [2]int32 |
| + Type int32 |
| + Handle string |
| + Owner uint32 |
| + Mode uint32 |
| +} |
| + |
| +type servingParkPersistentFile servingParkPersistentDirectory |
| + |
| +func validServingParkHandle(p servingParkPersistentDirectory) bool { |
| + b, err := hex.DecodeString(p.Handle) |
| + return err == nil && len(b) > 0 && len(b) <= 128 && hex.EncodeToString(b) == p.Handle && p.Type > 0 && p.Filesystem != [2]int32{} |
| +} |
| + |
| +func (p servingParkPersistentDirectory) valid() bool { |
| + return validServingParkHandle(p) && p.Mode&0170000 == 0040000 && p.Mode&0022 == 0 |
| +} |
| + |
| +func (p servingParkPersistentFile) valid() bool { |
| + return validServingParkHandle(servingParkPersistentDirectory(p)) && p.Mode&0170000 == 0100000 |
| +} |
| + |
| +// Historical records without handles remain readable. Partial new evidence |
| +// refuses rather than being treated as an old record eligible for recapture. |
| +func validateServingParkPersistentRecord(r servingParkRecord) error { |
| + p := r.Source.Persistent |
| + if p == (servingParkPersistentDirectory{}) { |
| + if r.Source.BootID != "" || r.EventPersistent != (servingParkPersistentDirectory{}) { |
| + return hostbox.ErrChanged |
| + } |
| + for _, f := range r.Source.Files { |
| + if f.Persistent != (servingParkPersistentFile{}) { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + return nil |
| + } |
| + if !p.valid() || !validServingParkBootID(r.Source.BootID) { |
| + return hostbox.ErrChanged |
| + } |
| + for _, f := range r.Source.Files { |
| + if f.Present { |
| + if !f.Persistent.valid() || f.Persistent.Filesystem != p.Filesystem { |
| + return hostbox.ErrChanged |
| + } |
| + } else if f.Persistent != (servingParkPersistentFile{}) { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + if r.Prepared { |
| + if !r.EventPersistent.valid() || r.EventPersistent.Filesystem != p.Filesystem || r.EventPersistent.Mode&0077 != 0 { |
| + return hostbox.ErrChanged |
| + } |
| + } else if r.EventPersistent != (servingParkPersistentDirectory{}) { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
| + |
| +func validServingParkBootID(id string) bool { |
| + if len(id) != 36 || id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' { |
| + return false |
| + } |
| + raw := strings.ReplaceAll(id, "-", "") |
| + b, err := hex.DecodeString(raw) |
| + return err == nil && len(b) == 16 && hex.EncodeToString(b) == raw && raw != strings.Repeat("0", 32) |
| +} |
| + |
| +// Only the saved source decides whether persistent evidence was captured. New |
| +// observations cannot upgrade an old intent or permanent legacy consumption. |
| +func sameServingParkSource(saved, fresh servingParkSource) bool { |
| + if saved.Persistent == (servingParkPersistentDirectory{}) && saved.BootID == "" { |
| + for _, item := range saved.Files { |
| + if item.Persistent != (servingParkPersistentFile{}) { |
| + return false |
| + } |
| + } |
| + fresh.Persistent = servingParkPersistentDirectory{} |
| + fresh.BootID = "" |
| + for i := range fresh.Files { |
| + fresh.Files[i].Persistent = servingParkPersistentFile{} |
| + } |
| + } |
| + return saved == fresh |
| +} |
diff --git a/internal/host/claimservingidentity_linux.go b/internal/host/claimservingidentity_linux.go| new file mode 100644 |
| index 000000000..2ca4dbca8 |
| --- /dev/null |
| +++ b/internal/host/claimservingidentity_linux.go |
| @@ -0,0 +1,53 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "encoding/hex" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// O_PATH pins the no-follow inode without opening a device or reading tenant |
| +// bytes. Compare the descriptor and name around the handle lookup. |
| +func servingParkPersistentArtifact(parent *os.File, expected servingParkFile) (servingParkPersistentFile, error) { |
| + var zero servingParkPersistentFile |
| + fd, err := unix.Openat(int(parent.Fd()), expected.Name, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return zero, err |
| + } |
| + defer unix.Close(fd) |
| + var st unix.Statx_t |
| + if err = unix.Statx(fd, "", unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &st); err != nil { |
| + return zero, err |
| + } |
| + if !servingParkStatxMatches(expected, st) { |
| + return zero, hostbox.ErrChanged |
| + } |
| + var fs unix.Statfs_t |
| + if err = unix.Fstatfs(fd, &fs); err != nil { |
| + return zero, err |
| + } |
| + h, mount, err := unix.NameToHandleAt(fd, "", unix.AT_EMPTY_PATH) |
| + if err != nil { |
| + return zero, err |
| + } |
| + out := servingParkPersistentFile{Filesystem: fs.Fsid.Val, Type: h.Type(), Handle: hex.EncodeToString(h.Bytes()), Owner: st.Uid, Mode: uint32(st.Mode)} |
| + if !out.valid() || mount <= 0 || uint64(mount) != expected.Mount { |
| + return zero, hostbox.ErrChanged |
| + } |
| + var again unix.Statx_t |
| + if err = unix.Statx(int(parent.Fd()), expected.Name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &again); err != nil { |
| + return zero, err |
| + } |
| + if !servingParkStatxMatches(expected, again) || again.Uid != st.Uid || again.Mode != st.Mode { |
| + return zero, hostbox.ErrChanged |
| + } |
| + return out, nil |
| +} |
| + |
| +func servingParkStatxMatches(expected servingParkFile, st unix.Statx_t) bool { |
| + return expected.Present && st.Mask&unix.STATX_MNT_ID != 0 && st.Mode&unix.S_IFMT == unix.S_IFREG && st.Nlink == 1 && unix.Mkdev(st.Dev_major, st.Dev_minor) == expected.Device && st.Ino == expected.Inode && st.Mnt_id == expected.Mount && st.Blocks <= ^uint64(0)/512 && st.Blocks*512 == expected.Bytes |
| +} |
diff --git a/internal/host/claimservinglegacy.go b/internal/host/claimservinglegacy.go| index be162307f..af42f56c0 100644 |
| --- a/internal/host/claimservinglegacy.go |
| +++ b/internal/host/claimservinglegacy.go |
| @@ -129,7 +129,7 @@ func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtif |
| } |
| func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool { |
| - if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent { |
| + if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent || c.Source.Persistent != r.Source.Persistent || c.Source.BootID != r.Source.BootID { |
| return false |
| } |
| for i, item := range c.Source.Files { |
diff --git a/internal/host/claimservinglegacyroot_linux.go b/internal/host/claimservinglegacyroot_linux.go| index 5f743ab77..13ffc6246 100644 |
| --- a/internal/host/claimservinglegacyroot_linux.go |
| +++ b/internal/host/claimservinglegacyroot_linux.go |
| @@ -258,11 +258,11 @@ func (p *servingParkRoot) parkLegacyServingDatabase(ctx context.Context, conn ne |
| } |
| func sameServingLegacySource(a, b servingParkSource) bool { |
| - if a.Directory != b.Directory || a.Parent != b.Parent { |
| + if a.Directory != b.Directory || a.Parent != b.Parent || a.Persistent != b.Persistent || a.BootID != b.BootID { |
| return false |
| } |
| for i := range a.Files { |
| - if !sameServingParkFile(a.Files[i], b.Files[i]) { |
| + if a.Files[i].Persistent != b.Files[i].Persistent || !sameServingParkFile(a.Files[i], b.Files[i]) { |
| return false |
| } |
| } |
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go| index 6b8cbd9ff..bc4d23e87 100644 |
| --- a/internal/host/claimservingpark_linux.go |
| +++ b/internal/host/claimservingpark_linux.go |
| @@ -233,11 +233,12 @@ func servingParkStat(parent *os.File, name string) (servingParkFile, error) { |
| return out, hostbox.ErrChanged |
| } |
| out.Present, out.Device, out.Inode, out.Mount, out.Bytes, out.Mode, out.Links = true, unix.Mkdev(st.Dev_major, st.Dev_minor), st.Ino, st.Mnt_id, st.Blocks*512, uint32(st.Mode)&unix.S_IFMT, st.Nlink |
| - return out, nil |
| + out.Persistent, err = servingParkPersistentArtifact(parent, out) |
| + return out, err |
| } |
| func sameServingParkFile(a, b servingParkFile) bool { |
| - return a.Name == b.Name && a.Present == b.Present && a.Device == b.Device && a.Inode == b.Inode && a.Mount == b.Mount && a.Mode == b.Mode && a.Links == b.Links && a.Bytes == b.Bytes |
| + return a.Name == b.Name && a.Present == b.Present && a.Device == b.Device && a.Inode == b.Inode && a.Mount == b.Mount && a.Mode == b.Mode && a.Links == b.Links && a.Bytes == b.Bytes && (a.Persistent == (servingParkPersistentFile{}) || a.Persistent == b.Persistent) |
| } |
| func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, error) { |
| @@ -248,6 +249,20 @@ func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, |
| } |
| defer source.Close() |
| out.Directory, out.Parent = a.Directory, parent |
| + out.BootID, err = f.currentBoot() |
| + if err != nil { |
| + return out, err |
| + } |
| + if f.handle == nil { |
| + return out, hostbox.ErrUnsupported |
| + } |
| + out.Persistent, err = f.handle(source) |
| + if err != nil { |
| + return out, err |
| + } |
| + if !out.Persistent.valid() { |
| + return out, hostbox.ErrChanged |
| + } |
| for i, suffix := range []string{"", ".gen", "-wal", "-shm"} { |
| file, err := servingParkStat(source, a.Successor.Host+".db"+suffix) |
| if err != nil { |
| @@ -283,6 +298,18 @@ func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, |
| return out, hostbox.ErrChanged |
| } |
| } |
| + if f.checkBoot(out.BootID) != nil { |
| + return out, hostbox.ErrChanged |
| + } |
| + again, pid, err := f.source(a) |
| + if err != nil { |
| + return out, err |
| + } |
| + defer again.Close() |
| + persistent, err := f.handle(again) |
| + if err != nil || pid != out.Parent || persistent != out.Persistent { |
| + return out, hostbox.ErrChanged |
| + } |
| return out, nil |
| } |
| @@ -365,6 +392,19 @@ func (f *servingParkFS) Prepare(r *servingParkRecord, save func() error) error { |
| if err = f.sync(int(root.Fd())); err != nil { |
| return err |
| } |
| + if r.Source.Persistent != (servingParkPersistentDirectory{}) { |
| + if f.checkBoot(r.Source.BootID) != nil || f.handle == nil { |
| + return hostbox.ErrChanged |
| + } |
| + persistent, e := f.handle(event) |
| + if e != nil { |
| + return e |
| + } |
| + if !persistent.valid() || persistent.Owner != f.owner || persistent.Mode&0077 != 0 || persistent.Filesystem != r.Source.Persistent.Filesystem { |
| + return hostbox.ErrChanged |
| + } |
| + r.EventPersistent = persistent |
| + } |
| r.Event, r.Prepared, r.Namespace = id, true, n.Root |
| return save() |
| } |
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index e716abb29..73357dd02 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -28,6 +28,7 @@ type servingParkLedger struct { |
| } |
| type servingParkFile struct { |
| + Persistent servingParkPersistentFile `json:",omitzero"` |
| Name string |
| Present bool |
| Device, Inode, Mount, Bytes uint64 |
| @@ -36,17 +37,20 @@ type servingParkFile struct { |
| } |
| type servingParkSource struct { |
| - Directory servingArtifactDirectory |
| - Parent servingArtifactDirectory |
| - Files [4]servingParkFile |
| + Persistent servingParkPersistentDirectory `json:",omitzero"` |
| + BootID string `json:",omitempty"` |
| + Directory servingArtifactDirectory |
| + Parent servingArtifactDirectory |
| + Files [4]servingParkFile |
| } |
| type servingParkRecord struct { |
| - CollectAfter time.Time `json:",omitempty"` |
| - Namespace servingArtifactDirectory `json:",omitempty"` |
| - LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| - Continuations []servingParkContinuation `json:",omitempty"` |
| - EventRemoveIntent, EventRemoved bool `json:",omitempty"` |
| + EventPersistent servingParkPersistentDirectory `json:",omitzero"` |
| + CollectAfter time.Time `json:",omitempty"` |
| + Namespace servingArtifactDirectory `json:",omitempty"` |
| + LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| + Continuations []servingParkContinuation `json:",omitempty"` |
| + EventRemoveIntent, EventRemoved bool `json:",omitempty"` |
| Version int |
| Authorization servingParkAuthorization |
| Source servingParkSource |
| @@ -136,6 +140,9 @@ func (l *servingParkLedger) read(id string) (servingParkRecord, error) { |
| } |
| func validateServingParkRecord(r servingParkRecord) error { |
| + if err := validateServingParkPersistentRecord(r); err != nil { |
| + return err |
| + } |
| a := r.Authorization |
| s := a.OriginalSuccessor |
| if r.Version != 1 || a.ID != servingRetirementID(a.Predecessor, s, a.Legacy) || s.Host == "" || s.Box == "" || s.Incarnation == "" || s.Generation == "" || s.Region == "" || s.Location == "" || s.Stamp == 0 || s.Stamp != s.Session || a.Successor != s || a.Directory != r.Source.Directory || r.Source.Directory.Inode == 0 || r.Source.Directory.Device == 0 || r.Source.Directory.Mount == 0 || r.Source.Parent.Inode == 0 || r.Source.Parent.Device != r.Source.Directory.Device || r.Source.Parent.Mount != r.Source.Directory.Mount { |
| @@ -421,9 +428,16 @@ func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servin |
| want := servingLegacyConsumption{Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy} |
| body, e := l.state.Read(key) |
| if e == nil { |
| - if servingParkDecode(body, &consumed) != nil || !reflect.DeepEqual(consumed, want) { |
| + if servingParkDecode(body, &consumed) != nil || !sameServingParkSource(consumed.Source, want.Source) { |
| + return zero, hostbox.ErrChanged |
| + } |
| + // A lost old-format intent keeps its original consumption |
| + // evidence. Never infer historical handles from current bytes. |
| + want.Source = consumed.Source |
| + if !reflect.DeepEqual(consumed, want) { |
| return zero, hostbox.ErrChanged |
| } |
| + r.Source = consumed.Source |
| } else if !errors.Is(e, os.ErrNotExist) { |
| return zero, e |
| } |
| @@ -465,7 +479,7 @@ func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servin |
| return zero, hostbox.ErrChanged |
| } |
| fresh, e := l.fs.Capture(a) |
| - if e != nil || fresh != r.Source { |
| + if e != nil || !sameServingParkSource(r.Source, fresh) { |
| return zero, hostbox.ErrChanged |
| } |
| if slot.ID != "" { |
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go| index d5df3d135..60396684b 100644 |
| --- a/internal/host/claimservingpark_rootrunner_linux_test.go |
| +++ b/internal/host/claimservingpark_rootrunner_linux_test.go |
| @@ -17,6 +17,7 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/rootstate" |
| "golang.org/x/sys/unix" |
| @@ -125,8 +126,19 @@ func servingRunnerPark(t *testing.T, data string, state rootstate.Store) serving |
| if err != nil { |
| t.Fatal(err) |
| } |
| - r := servingParkRecord{Version: 1, Authorization: a, Source: source, Disposition: servingParkRetain} |
| - save := func() error { return (&servingParkLedger{state: state}).write("fixture.json", r) } |
| + // Stop authorization remains explicitly fixture-backed. It is encoded in |
| + // the production record format so the reboot inspector reads the ledger. |
| + at := time.Now().UTC() |
| + target := hostguard.FenceTarget{Host: current.Host, Box: current.Box, Region: current.Region, Incarnation: current.Incarnation, Stamp: 1, Session: 1, FenceID: "runner-stop", Enrollment: "runner-enrollment", BootID: source.BootID, ControllerID: "runner-controller", DirectoryRevision: "1", ClaimETag: "runner-claim", ClaimGen: 1} |
| + receipt := StopReceipt{V: 1, ID: target.FenceID, Kind: StopReceiptStopped, Region: target.Region, Host: target.Host, Box: target.Box, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, FenceID: target.FenceID, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, DirectoryRevision: target.DirectoryRevision, ClaimETag: target.ClaimETag, ClaimGen: target.ClaimGen, EvidenceAt: at} |
| + r := servingParkRecord{Version: 1, Authorization: a, Source: source, Disposition: servingParkRetain, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(receipt), EvidenceAt: at}}} |
| + save := func() error { |
| + l := servingParkLedger{state: state} |
| + if err := l.save(r); err != nil { |
| + return err |
| + } |
| + return l.write("fixture.json", r) |
| + } |
| if err = save(); err != nil { |
| t.Fatal(err) |
| } |
| @@ -178,6 +190,17 @@ func TestRootServingParkUnitPayload(t *testing.T) { |
| if _, err := fs.inspectRebootTopology(t.Context()); err != nil { |
| t.Fatal("installed-unit topology inspection", err) |
| } |
| + beforeRecord, err := state.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := fs.inspectRebootRecord(t.Context(), r.Authorization.ID); err != nil { |
| + t.Fatal("installed-unit record inspection", err) |
| + } |
| + afterRecord, err := state.Read("retirement-" + r.Authorization.ID + ".json") |
| + if err != nil || !bytes.Equal(beforeRecord, afterRecord) { |
| + t.Fatal("record inspection changed history", err) |
| + } |
| fs.boot = servingParkBootID |
| r.Deleting, r.Cause = true, "expiry" |
| save := func() error { return (&servingParkLedger{state: state}).write("fixture.json", r) } |
| @@ -417,7 +440,36 @@ func TestRootRebootServingParkChangedMountPhase2(t *testing.T) { |
| if err != nil || !bytes.Equal(parentBefore, parentAfter) { |
| t.Fatal("inspection rewrote parent authority", err) |
| } |
| - t.Log("persistent directory handles match across actual reboot; retirement authority remains unchanged") |
| + beforeRecord, err := state.Read("retirement-" + saved.Record.Authorization.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + recordObservation, err := fs.inspectRebootRecord(t.Context(), saved.Record.Authorization.ID) |
| + if err != nil || recordObservation.Source.Mount != observation.CurrentParent.Mount || recordObservation.Event.Mount != observation.CurrentRoot.Mount { |
| + t.Fatal("persistent source/event/artifacts after reboot", recordObservation, err) |
| + } |
| + // Model a reused file inode by changing only the saved persistent handle. |
| + // The same device/inode/size must not suffice to identify an old artifact. |
| + corrupted := saved.Record |
| + corrupted.Source.Files[0].Persistent.Handle = "aa" |
| + ledger := servingParkLedger{state: state} |
| + if err := ledger.save(corrupted); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := fs.inspectRebootRecord(t.Context(), corrupted.Authorization.ID); err == nil { |
| + t.Fatal("changed persistent artifact handle accepted") |
| + } |
| + if err := state.Write("retirement-"+saved.Record.Authorization.ID+".json", beforeRecord); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := fs.inspectRebootRecord(t.Context(), saved.Record.Authorization.ID); err != nil { |
| + t.Fatal("restored read-only control", err) |
| + } |
| + afterRecord, err := state.Read("retirement-" + saved.Record.Authorization.ID + ".json") |
| + if err != nil || !bytes.Equal(beforeRecord, afterRecord) { |
| + t.Fatal("record inspection changed history", err) |
| + } |
| + t.Log("persistent parent/root/source/event/file handles match across actual reboot; retirement authority remains unchanged") |
| if err := fs.CheckParked(&saved.Record); !errors.Is(err, hostbox.ErrChanged) { |
| t.Fatalf("old mount authority reused: %v", err) |
| } |
diff --git a/internal/host/claimservingreboot_linux.go b/internal/host/claimservingreboot_linux.go| new file mode 100644 |
| index 000000000..a1b05a5f7 |
| --- /dev/null |
| +++ b/internal/host/claimservingreboot_linux.go |
| @@ -0,0 +1,198 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "io" |
| + "os" |
| + "reflect" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// This observation is not filesystem or transport authority. The eventual |
| +// transaction must retain Record and commit separate current-boot identities. |
| +type servingParkRebootRecord struct { |
| + Topology servingParkRebootObservation |
| + Record servingParkRecord |
| + Source, Event servingArtifactDirectory |
| +} |
| + |
| +// The caller holds namespace.lock. Repeating the whole inspection rejects a |
| +// source/name change during handle reads and a concurrent private-record change. |
| +func (f *servingParkFS) inspectRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { |
| + var zero servingParkRebootRecord |
| + first, err := f.observeRebootRecord(ctx, id) |
| + if err != nil { |
| + return zero, err |
| + } |
| + again, err := f.observeRebootRecord(ctx, id) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if !reflect.DeepEqual(first, again) { |
| + return zero, hostbox.ErrChanged |
| + } |
| + if err := ctx.Err(); err != nil { |
| + return zero, err |
| + } |
| + return again, nil |
| +} |
| + |
| +func (f *servingParkFS) observeRebootRecord(ctx context.Context, id string) (servingParkRebootRecord, error) { |
| + var out servingParkRebootRecord |
| + topology, err := f.inspectRebootTopology(ctx) |
| + if err != nil { |
| + return out, err |
| + } |
| + r, err := (&servingParkLedger{state: f.state}).read(id) |
| + if err != nil { |
| + return out, err |
| + } |
| + n := topology.Previous |
| + if r.Source.BootID != n.BootID || !r.Source.Persistent.valid() || r.Source.Parent != n.Parent || r.Source.Persistent.Filesystem != n.Topology.Parent.Filesystem || r.Prepared && r.Namespace != n.Root { |
| + return out, hostbox.ErrChanged |
| + } |
| + parent, pid, err := f.parent() |
| + if err != nil { |
| + return out, err |
| + } |
| + defer parent.Close() |
| + root, rid, err := f.privateDir(parent, ".serving-parked") |
| + if err != nil { |
| + return out, err |
| + } |
| + defer root.Close() |
| + if pid != topology.CurrentParent || rid != topology.CurrentRoot { |
| + return out, hostbox.ErrChanged |
| + } |
| + current := servingParkNamespace{Version: 1, Parent: pid, Root: rid, BootID: topology.BootID} |
| + handles, err := f.captureTopology(parent, root, current) |
| + if err != nil { |
| + return out, err |
| + } |
| + if *handles != *n.Topology { |
| + return out, hostbox.ErrChanged |
| + } |
| + var source *os.File |
| + // Completed parks are independent of the live installation, which may have |
| + // restored a successor or been removed. Incomplete parks still require it. |
| + if !r.Complete { |
| + source, err = servingParkOpenDir(parent, provision.InstanceID(r.Authorization.Successor.Host)) |
| + if err != nil { |
| + return out, err |
| + } |
| + defer source.Close() |
| + out.Source, err = f.rebootDirectory(source, r.Source.Directory, r.Source.Persistent, pid.Mount) |
| + if err != nil { |
| + return out, err |
| + } |
| + } |
| + event, eid, err := f.privateDir(root, r.Authorization.ID) |
| + if errors.Is(err, unix.ENOENT) { |
| + if r.Prepared && !r.EventRemoveIntent { |
| + return out, hostbox.ErrChanged |
| + } |
| + } else if err != nil { |
| + return out, err |
| + } else { |
| + defer event.Close() |
| + if !r.Prepared || r.EventRemoved { |
| + return out, hostbox.ErrChanged |
| + } |
| + out.Event, err = f.rebootDirectory(event, r.Event, r.EventPersistent, pid.Mount) |
| + if err != nil || out.Event != eid { |
| + return out, hostbox.ErrChanged |
| + } |
| + // Only four fixed artifact names can be evidence. Unknown children remain |
| + // untouched and block this event, including after an interrupted unlink. |
| + names, e := event.Readdirnames(5) |
| + if len(names) > 4 || e != nil && !errors.Is(e, io.EOF) { |
| + return out, hostbox.ErrChanged |
| + } |
| + for _, name := range names { |
| + known := false |
| + for _, item := range r.Source.Files { |
| + if item.Present && !item.Deleted && item.Name == name { |
| + known = true |
| + } |
| + } |
| + if !known { |
| + return out, hostbox.ErrChanged |
| + } |
| + } |
| + } |
| + for _, item := range r.Source.Files { |
| + src := servingParkFile{Name: item.Name} |
| + dst := servingParkFile{Name: item.Name} |
| + if source != nil { |
| + src, err = servingParkStat(source, item.Name) |
| + if err != nil { |
| + return out, err |
| + } |
| + } |
| + if event != nil { |
| + dst, err = servingParkStat(event, item.Name) |
| + if err != nil { |
| + return out, err |
| + } |
| + } |
| + if !servingParkRebootFileMatches(item, src, dst, pid.Mount, r) { |
| + return out, hostbox.ErrChanged |
| + } |
| + } |
| + if err := f.checkBoot(topology.BootID); err != nil { |
| + return out, err |
| + } |
| + if err := ctx.Err(); err != nil { |
| + return out, err |
| + } |
| + out.Topology, out.Record = topology, r |
| + return out, nil |
| +} |
| + |
| +func (f *servingParkFS) rebootDirectory(d *os.File, old servingArtifactDirectory, proof servingParkPersistentDirectory, mount uint64) (servingArtifactDirectory, error) { |
| + var zero servingArtifactDirectory |
| + id, err := servingParkDirID(d) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if !proof.valid() || id.Device != old.Device || id.Inode != old.Inode || id.Mount != mount { |
| + return zero, hostbox.ErrChanged |
| + } |
| + actual, err := f.handle(d) |
| + if err != nil { |
| + return zero, err |
| + } |
| + if actual != proof { |
| + return zero, hostbox.ErrChanged |
| + } |
| + return id, nil |
| +} |
| + |
| +func servingParkRebootFileMatches(old, src, dst servingParkFile, mount uint64, r servingParkRecord) bool { |
| + if !old.Present { |
| + return !src.Present && !dst.Present |
| + } |
| + if !old.Persistent.valid() { |
| + return false |
| + } |
| + expected := old |
| + expected.Mount = mount |
| + if old.Deleted || r.EventRemoved { |
| + return !src.Present && !dst.Present |
| + } |
| + if r.Deleting { |
| + return !src.Present && (sameServingParkFile(expected, dst) || !dst.Present && old.DeleteIntent) |
| + } |
| + if r.Parked || old.Moved { |
| + return !src.Present && sameServingParkFile(expected, dst) |
| + } |
| + // A rename can be durable before its progress write. Accept exactly one |
| + // recorded inode, with its handle; a second link or a replacement refuses. |
| + return sameServingParkFile(expected, src) && !dst.Present || !src.Present && sameServingParkFile(expected, dst) |
| +} |
diff --git a/internal/host/claimservingreboot_linux_test.go b/internal/host/claimservingreboot_linux_test.go| new file mode 100644 |
| index 000000000..d60a4ef73 |
| --- /dev/null |
| +++ b/internal/host/claimservingreboot_linux_test.go |
| @@ -0,0 +1,429 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func servingRebootRecordFixture(t *testing.T, phase string) (*servingParkDiskFixture, servingParkRecord) { |
| + t.Helper() |
| + f := newServingParkDiskFixture(t) |
| + a, receipts := f.evidence(1, true, "missing") |
| + f.put("", "last database") |
| + f.put("-wal", "last WAL") |
| + f.put("-shm", "last SHM") |
| + if err := f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err != nil { |
| + t.Fatal(err) |
| + } |
| + root, _, err := f.fs.namespace() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + root.Close() |
| + var r servingParkRecord |
| + if phase == "captured" || phase == "prepared" || phase == "rename-reply" { |
| + r, err = f.ledger.capture(t.Context(), a, receipts, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = f.ledger.save(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + save := func() error { return f.ledger.save(r) } |
| + if phase != "captured" { |
| + if err = f.fs.Prepare(&r, save); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if phase == "rename-reply" { |
| + f.fs.rename = func(from int, old string, to int, name string, flags uint) error { |
| + if err := unix.Renameat2(from, old, to, name, flags); err != nil { |
| + return err |
| + } |
| + return unix.EIO |
| + } |
| + if err = f.fs.Move(&r, save); !errors.Is(err, unix.EIO) { |
| + t.Fatal("missing lost rename reply", err) |
| + } |
| + f.fs.rename = unix.Renameat2 |
| + } |
| + } else { |
| + r, err = f.park(a, receipts) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if phase != "parked" { |
| + f.complete(a) |
| + } |
| + if phase == "complete" { |
| + f.put("", "successor database") |
| + } |
| + if phase == "unlink-reply" || phase == "partial-unlink" || phase == "rmdir-reply" || phase == "closed" { |
| + f.now = f.now.Add(8 * 24 * time.Hour) |
| + calls := 0 |
| + f.fs.unlink = func(fd int, name string, flags int) error { |
| + calls++ |
| + if phase == "partial-unlink" && calls == 2 { |
| + return unix.EIO |
| + } |
| + if err := unix.Unlinkat(fd, name, flags); err != nil { |
| + return err |
| + } |
| + if phase == "unlink-reply" || phase == "rmdir-reply" && flags == unix.AT_REMOVEDIR { |
| + return unix.EIO |
| + } |
| + return nil |
| + } |
| + err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }) |
| + if phase == "closed" && err != nil || phase != "closed" && !errors.Is(err, unix.EIO) { |
| + t.Fatal(phase, err) |
| + } |
| + f.fs.unlink = unix.Unlinkat |
| + } |
| + } |
| + r, err = f.ledger.read(a.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.fs.boot = func() (string, error) { return servingOtherBoot, nil } |
| + return f, r |
| +} |
| + |
| +func TestServingParkRebootRecordReadOnly(t *testing.T) { |
| + for _, phase := range []string{"captured", "prepared", "rename-reply", "parked", "complete", "unlink-reply", "partial-unlink", "rmdir-reply", "closed"} { |
| + t.Run(phase, func(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, phase) |
| + before := servingBootSnapshot(t, f.disk.dir) |
| + var got servingParkRebootRecord |
| + err := f.ledger.withNamespace(t.Context(), func() error { |
| + var err error |
| + got, err = f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) |
| + return err |
| + }) |
| + if err != nil || !reflect.DeepEqual(got.Record, r) || got.Topology.BootID != servingOtherBoot { |
| + t.Fatal("record inspection", got, err) |
| + } |
| + if r.Complete && got.Source != (servingArtifactDirectory{}) { |
| + t.Fatal("completed park depended on live source") |
| + } |
| + if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) { |
| + t.Fatal("inspection wrote authority") |
| + } |
| + if err := f.fs.CheckParked(&r); err == nil { |
| + t.Fatal("observation granted old-boot effects") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkRebootRecordRefusesChangedEvidence(t *testing.T) { |
| + for _, fault := range []string{"no-source-proof", "no-file-proof", "no-event-proof", "source-boot", "source-handle", "event-handle", "file-handle", "file-filesystem", "file-mode", "event-mode", "namespace", "source-replaced", "event-replaced", "file-replaced", "file-hardlink", "file-symlink", "unexpected-child", "missing-file", "duplicate-source", "old-format", "source-race", "event-race", "record-race", "cancel-race"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "parked") |
| + event := filepath.Dir(f.path(r.Authorization, "")) |
| + source := filepath.Dir(provisionDBPath(f)) |
| + ctx, cancel := context.WithCancel(t.Context()) |
| + defer cancel() |
| + switch fault { |
| + case "no-source-proof": |
| + r.Source.Persistent = servingParkPersistentDirectory{} |
| + case "no-file-proof": |
| + r.Source.Files[0].Persistent = servingParkPersistentFile{} |
| + case "no-event-proof": |
| + r.EventPersistent = servingParkPersistentDirectory{} |
| + case "source-boot": |
| + r.Source.BootID = servingOtherBoot |
| + case "source-handle": |
| + r.Source.Persistent.Handle = "aa" |
| + case "event-handle": |
| + r.EventPersistent.Handle = "aa" |
| + case "file-handle": |
| + r.Source.Files[0].Persistent.Handle = "aa" |
| + case "file-filesystem": |
| + r.Source.Files[0].Persistent.Filesystem[0]++ |
| + case "file-mode": |
| + r.Source.Files[0].Persistent.Mode ^= 0100 |
| + case "event-mode": |
| + r.EventPersistent.Mode ^= 0100 |
| + case "namespace": |
| + r.Namespace.Inode++ |
| + case "source-replaced", "event-replaced": |
| + path := source |
| + if fault == "event-replaced" { |
| + path = event |
| + } |
| + if err := os.Rename(path, path+"-old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "file-replaced", "file-hardlink", "file-symlink", "missing-file": |
| + path := f.path(r.Authorization, "") |
| + if fault == "file-hardlink" { |
| + if err := os.Link(path, path+"-link"); err != nil { |
| + t.Fatal(err) |
| + } |
| + break |
| + } |
| + if err := os.Rename(path, path+"-old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "file-symlink" { |
| + if err := os.Symlink(path+"-old", path); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if fault == "file-replaced" { |
| + if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + // Put the retained original outside the event so unknown-child checks |
| + // cannot hide a missing/replaced-file validation bug. |
| + if err := os.Rename(path+"-old", source+"/saved-original"); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "unexpected-child": |
| + if err := os.WriteFile(filepath.Join(event, "unknown"), []byte("retain"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "duplicate-source": |
| + f.put("", "replacement") |
| + case "old-format": |
| + r.Source.Persistent = servingParkPersistentDirectory{} |
| + r.Source.BootID = "" |
| + r.EventPersistent = servingParkPersistentDirectory{} |
| + for i := range r.Source.Files { |
| + r.Source.Files[i].Persistent = servingParkPersistentFile{} |
| + } |
| + case "source-race", "event-race", "record-race", "cancel-race": |
| + calls := 0 |
| + f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { |
| + p, err := servingParkPersistentDir(d) |
| + calls++ |
| + // The first topology inspection takes four handles; the pinned |
| + // topology takes two, followed by source and event. |
| + if calls == 8 { |
| + switch fault { |
| + case "cancel-race": |
| + cancel() |
| + case "record-race": |
| + changed := r |
| + changed.Receipts = nil |
| + if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", changed); err != nil { |
| + t.Fatal(err) |
| + } |
| + default: |
| + path := source |
| + if fault == "event-race" { |
| + path = event |
| + } |
| + if err := os.Rename(path, path+"-old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(path, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + return p, err |
| + } |
| + } |
| + if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := servingBootSnapshot(t, f.disk.dir) |
| + got, err := f.fs.inspectRebootRecord(ctx, r.Authorization.ID) |
| + if err == nil || !reflect.DeepEqual(got, servingParkRebootRecord{}) { |
| + t.Fatal("accepted changed record", got, err) |
| + } |
| + if fault != "record-race" { |
| + if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) { |
| + t.Fatal("refusal wrote authority") |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func provisionDBPath(f *servingParkDiskFixture) string { |
| + return filepath.Join(f.data, "instances", provision.InstanceID(f.base.Host), f.base.Host+".db") |
| +} |
| + |
| +func TestServingParkPersistentEvidenceNeverUpgradesOldIntent(t *testing.T) { |
| + for _, phase := range []string{"intent", "consumption"} { |
| + t.Run(phase, func(t *testing.T) { |
| + f := newServingParkDiskFixture(t) |
| + a, receipts := f.evidence(1, true, "missing") |
| + f.put("", "legacy") |
| + r, err := f.ledger.capture(t.Context(), a, receipts, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r.Source.Persistent = servingParkPersistentDirectory{} |
| + r.Source.BootID = "" |
| + for i := range r.Source.Files { |
| + r.Source.Files[i].Persistent = servingParkPersistentFile{} |
| + } |
| + if phase == "intent" { |
| + err = f.ledger.save(r) |
| + } else { |
| + err = f.ledger.write(servingLegacyKey("legacy", a.Successor), servingLegacyConsumption{Authorization: a, Source: r.Source, Receipts: receipts}) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + parked, err := f.park(a, receipts) |
| + if err != nil { |
| + t.Fatal("old intent replay", err) |
| + } |
| + if parked.Source.Persistent != (servingParkPersistentDirectory{}) || parked.Source.BootID != "" || parked.EventPersistent != (servingParkPersistentDirectory{}) { |
| + t.Fatal("upgraded old evidence") |
| + } |
| + f.complete(a) |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkRebootFileRequiresDeleteIntent(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "unlink-reply") |
| + r.Source.Files[0].DeleteIntent = false |
| + if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatal("absent artifact without delete intent accepted", err) |
| + } |
| +} |
| + |
| +func TestServingParkRebootRecordChangedMount(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "rename-reply") |
| + oldMount := r.Source.Parent.Mount + 123 |
| + r.Source.Parent.Mount = oldMount |
| + r.Source.Directory.Mount = oldMount |
| + r.Authorization.Directory.Mount = oldMount |
| + r.Event.Mount = oldMount |
| + r.Namespace.Mount = oldMount |
| + for i := range r.Source.Files { |
| + if r.Source.Files[i].Present { |
| + r.Source.Files[i].Mount = oldMount |
| + } |
| + } |
| + if err := f.ledger.save(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + parent, err := f.fs.namespaceParent() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + parent.Parent.Mount = oldMount |
| + if err := f.ledger.write("namespace-parent.json", parent); err != nil { |
| + t.Fatal(err) |
| + } |
| + var n servingParkNamespace |
| + b, err := f.disk.Read("namespace.json") |
| + if err != nil || servingParkDecode(b, &n) != nil { |
| + t.Fatal(err) |
| + } |
| + n.Parent.Mount = oldMount |
| + n.Root.Mount = oldMount |
| + if err := f.ledger.write("namespace.json", n); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) |
| + if err != nil || got.Source.Mount == oldMount || got.Event.Mount == oldMount || got.Source.Mount != got.Event.Mount || !reflect.DeepEqual(got.Record, r) { |
| + t.Fatal("changed-mount inspection", got, err) |
| + } |
| +} |
| + |
| +func TestServingParkRebootCompletedDoesNotReadSource(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "complete") |
| + path := filepath.Dir(provisionDBPath(f)) |
| + if err := os.Rename(path, path+"-removed"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(path+"-removed", path); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID) |
| + if err != nil || got.Source != (servingArtifactDirectory{}) { |
| + t.Fatal("completed park consulted source", got, err) |
| + } |
| +} |
| + |
| +func TestServingParkPersistentLegacyConsumption(t *testing.T) { |
| + f, r := servingRebootRecordFixture(t, "parked") |
| + c, err := f.ledger.legacyConsumption(r.Authorization.Successor) |
| + if err != nil || c == nil { |
| + t.Fatal(err) |
| + } |
| + if !sameServingLegacyConsumption(*c, r) || !sameServingLegacySource(r.Source, c.Source) { |
| + t.Fatal("fixture consumption mismatch") |
| + } |
| + old := r.Source |
| + old.Persistent = servingParkPersistentDirectory{} |
| + old.BootID = "" |
| + for i := range old.Files { |
| + old.Files[i].Persistent = servingParkPersistentFile{} |
| + } |
| + partial := old |
| + partial.Files[0].Persistent = r.Source.Files[0].Persistent |
| + if sameServingLegacySource(old, partial) { |
| + t.Fatal("partial consumed proof accepted as old evidence") |
| + } |
| + for _, field := range []string{"boot", "directory", "file"} { |
| + t.Run(field, func(t *testing.T) { |
| + changed := *c |
| + switch field { |
| + case "boot": |
| + changed.Source.BootID = servingOtherBoot |
| + case "directory": |
| + changed.Source.Persistent.Handle = "aa" |
| + case "file": |
| + changed.Source.Files[0].Persistent.Handle = "aa" |
| + } |
| + if sameServingLegacyConsumption(changed, r) || sameServingLegacySource(r.Source, changed.Source) { |
| + t.Fatal("changed immutable consumption accepted") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkPersistentEventCaptureFailure(t *testing.T) { |
| + f := newServingParkDiskFixture(t) |
| + a, receipts := f.evidence(1, true, "missing") |
| + f.put("", "preserve source") |
| + f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { |
| + if d.Name() == a.ID { |
| + return servingParkPersistentDirectory{}, unix.EIO |
| + } |
| + return servingParkPersistentDir(d) |
| + } |
| + if _, err := f.park(a, receipts); !errors.Is(err, unix.EIO) { |
| + t.Fatal("event capture failure", err) |
| + } |
| + r, err := f.ledger.read(a.ID) |
| + if err != nil || r.Prepared || r.EventPersistent != (servingParkPersistentDirectory{}) { |
| + t.Fatal("failed capture gained event authority", err) |
| + } |
| + body, err := os.ReadFile(provisionDBPath(f)) |
| + if err != nil || string(body) != "preserve source" { |
| + t.Fatal("capture failure changed source", err) |
| + } |
| + f.fs.handle = servingParkPersistentDir |
| + if _, err := f.park(a, receipts); err != nil { |
| + t.Fatal("same-boot event capture recovery", err) |
| + } |
| +} |
diff --git a/internal/host/claimservingtopology_linux.go b/internal/host/claimservingtopology_linux.go| index c4de00cb0..ee7d649b5 100644 |
| --- a/internal/host/claimservingtopology_linux.go |
| +++ b/internal/host/claimservingtopology_linux.go |
| @@ -12,26 +12,10 @@ import ( |
| "golang.org/x/sys/unix" |
| ) |
| -// File handles distinguish a retained inode from a later reuse of its number. |
| -// They are meaningful only within the filesystem that issued them. Neither a |
| -// mount number nor a device number alone identifies that filesystem after boot. |
| -type servingParkPersistentDirectory struct { |
| - Filesystem [2]int32 |
| - Type int32 |
| - Handle string |
| - Owner uint32 |
| - Mode uint32 |
| -} |
| - |
| type servingParkTopology struct { |
| Parent, Root servingParkPersistentDirectory |
| } |
| -func (p servingParkPersistentDirectory) valid() bool { |
| - b, err := hex.DecodeString(p.Handle) |
| - return err == nil && len(b) > 0 && len(b) <= 128 && hex.EncodeToString(b) == p.Handle && p.Type > 0 && p.Filesystem != [2]int32{} && p.Mode&unix.S_IFMT == unix.S_IFDIR && p.Mode&0022 == 0 |
| -} |
| - |
| // This only obtains a handle for an already-open directory. It never opens a |
| // supplied handle, follows a request path or changes mount permissions. |
| func servingParkPersistentDir(d *os.File) (servingParkPersistentDirectory, error) { |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index 3fe6cc933..404ba08ec 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-seed.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-seed.tsv |
| @@ -1,4 +1,4 @@ |
| -# Files seed checkpoint 20 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 21 overlay; pending Paul’s census ruling. |
| # Database retirement activation remains refused; serving installation transfer uses the existing root transport. |
| # Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed. |
| # Every classification below is rev3=PROPOSED-seed, not an approved disposition. |
| @@ -32,26 +32,26 @@ internal/host/claimservingretire.go servingRetirementID authority C,J4 rev3=PROP |
| internal/host/claimservingretire.go servingWriterIdentity authority C,J4 rev3=PROPOSED-seed; authoritative serving holder and unique cold admission; exact local executor, stamp, session, generation, incarnation, location and region file:Exec@403 f7d8c868ce9856eab448aa72bdf20ee02cb70179ce0f435d45888f72e0c3b7bf |
| 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/host/claimservingpark_linux.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; root-only filesystem dependencies, fixed nosymfollow validation and private state store; bound through private root service construction; production boot identity reader is fixed at construction; persistent file-handle reader fixed at production construction 30278f0fb52daaea0247eecaeeeb23aef50051f752817aca3681333c4e538293 |
| -internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, mount, device, inode, regular type, single link and allocated-byte identity d5d684356e793dcfd55fe1ec9c609f2320e0155e052caca42475348075e8fd68 |
| +internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, mount, device, inode, regular type, single link and allocated-byte identity; new recorded files also require the original filesystem handle, owner and mode; old saved identities retain their existing checks 58b2d78cd6a6d35e1da3e43d9f9761a6986da63ff6fb171e73c3874746674dae |
| internal/host/claimservingpark_linux.go servingParkDirID authority C,J4 rev3=PROPOSED-seed; descriptor statx directory type, device, inode and mount identity file:Statx@102 3480dad6d65fedcc17e678df38cf13352ad3c4a8a9d0a09a395973d92b98094e |
| -internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor file:Fstat@920,file:Openat@694,file:ReadAll@1138 c3a9348ddd9548f3fcfdcd45abc42b6a758a6399ed34e92481785e26c110763d |
| +internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor; source boot and persistent directory handle captured before mutation and rechecked after all artifact reads file:Fstat@1199,file:Openat@973,file:ReadAll@1417 1a0278ee90f2d1b7f3a604ba2b6f1e17a8c4e0b2b0a35c6b4f3f0667b78a8c91 |
| internal/host/claimservingpark_linux.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; all recorded event identities, unexpected children and source absence before root completion file:Readdirnames@1266 ac675e110e1493ab1ec69359696d462228f1e1265923c2c0d471111851a2c645 |
| internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and complete artifact set before journaled unlink; persist proven namespace identity before old-record deletion; remove only the recorded empty event file:Readdirnames@402 7fa2b1a5f32a7c67eceb48e6eee5adaef44fbb0c065e05a21fcd60391f40ca34 |
| internal/host/claimservingpark_linux.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; recorded source/destination inodes and explicit absences; no-replace same-mount rename, post-rename identity and both parent fsyncs before progress 6e43ae58b6a02d167135fabdde7ade06502511c5237b313e6ca6c2a8e9a2c8d0 |
| -internal/host/claimservingpark_linux.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event; persist namespace and event identities together; old prepared events gain a namespace only after exact identity proof file:Prepare@24,file:Readdirnames@967 1fddbfb85e6381113db4807f41890dac36a104997094a139408eca75b4572eab |
| +internal/host/claimservingpark_linux.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; recorded private namespace and empty event; persist namespace and event identities together; old prepared events gain a namespace only after exact identity proof; new intents record the private event filesystem handle before the first rename; old records gain no persistent evidence file:Prepare@24,file:Readdirnames@967 653a70287033f46e8b898d5b3ef7fdec2af31f399e8bebc248e2d3b11e23517c |
| internal/host/claimservingpark_linux.go servingParkFS.event authority C,J4 rev3=PROPOSED-seed; current or proven displaced namespace and exact private event mount/device/inode; old records may fall back only to the previous root recorded before quarantine 625ed442442a9b07c57392f72dea64e9e91eba65d2e2ef0e9899275756336788 |
| internal/host/claimservingpark_linux.go servingParkFS.namespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace and quarantine completion records, effective parent mount and root-owned park directory; pending quarantine blocks namespace creation and use; unknown roots refused without adoption; the saved namespace boot must equal the current kernel boot before opening or changing park contents; new namespace publication includes persistent parent/root handles before any event creation file:Read@282 768954e03ac18ebb0e28b04ebb0ad0e1e09a546fee93476367e4be1679b08f1a |
| internal/host/claimservingpark_linux.go servingParkFS.parent authority C,J4 rev3=PROPOSED-seed; deployment-controlled instances path, descriptor mount identity and effective nosymfollow topology; path replacement refuses 1d241a7d446bbbade6e460efd911863f8a5246b8ac1d47b2548034e3feb2f216 |
| internal/host/claimservingpark_linux.go servingParkFS.privateDir authority C,J4 rev3=PROPOSED-seed; no-follow directory descriptor, exact private owner and mode plus device/inode/mount identity file:Fstat@261 8a4a33c6871fa88cd53f9922f99e173102966827bfea3998b2b3bd749a2ad11b |
| internal/host/claimservingpark_linux.go servingParkFS.source authority C,J4 rev3=PROPOSED-seed; canonical host-derived nested state directory checked against independently supplied root directory identity; no registry path b7a30539c735e811c366ebb7c853a962500e82305ba1caa64f73402ae6edbce5 |
| internal/host/claimservingpark_linux.go servingParkOpenDir authority C,J4 rev3=PROPOSED-seed; single basename opened relative to a pinned parent with no-follow directory semantics file:Openat@210 665ff3adc3e1cd1c6c4b92439160a0ef8013ea8e35375bcdfa90747888751ff9 |
| -internal/host/claimservingpark_linux.go servingParkStat authority C,J4 rev3=PROPOSED-seed; no-follow artifact statx identity, single links, explicit absence and checked allocated-byte count file:Statx@143 2c5d25be8581568148d13e3cad6f4b5f861fec8bff2545502fbdea0d5d949bb6 |
| +internal/host/claimservingpark_linux.go servingParkStat authority C,J4 rev3=PROPOSED-seed; no-follow artifact statx identity, single links, explicit absence and checked allocated-byte count; regular files additionally obtain a descriptor-pinned persistent filesystem handle file:Statx@143 52bf86dad4d27ade2cb85ddff8faa7bf5a12bf34706641855dc4b43c06bfa9d0 |
| internal/host/claimservingpark_other.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; production boot identity reader is fixed at construction; persistent file-handle reader fixed at production construction 69baa4c747e687578e73b8369f2df9987fe6fb5836b78796cc41c776ba12c802 |
| -internal/host/claimservingpark_other.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority a5560b338356792386a5a1b2e27cb08ca0d5aa024cd03b567a78dfd986ce7871 |
| +internal/host/claimservingpark_other.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; source boot and persistent directory handle captured before mutation and rechecked after all artifact reads a5560b338356792386a5a1b2e27cb08ca0d5aa024cd03b567a78dfd986ce7871 |
| internal/host/claimservingpark_other.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 3a8cb4d8052bbdea8cf7a300944de72c68012e6efa252c293fc1dc06a3a2150e |
| internal/host/claimservingpark_other.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and artifact identities; validates complete child set before journaled unlink, then removes only the recorded empty event with durable intent and progress 8c169e8428b913d2f05b502ce971be49d5febaad95d8ae811aae21c81f3434ce |
| internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 9d215b168da646166104db5624727038f793d2ae0c40eb71f23883f12b6b4b13 |
| -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_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; new intents record the private event filesystem handle before the first rename; old records gain no persistent evidence 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 through typed journal completion 00f9c10b8130c134a41dea74848aa1651ae3331c8440a81e1b3f59f54f6a7218 |
| @@ -64,7 +64,7 @@ internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 |
| 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 through the typed journal service 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; private immutable authorization, source/event identities and durable progress; collection deadline requires complete closed removal and known namespace, no earlier than parked time plus transport receipt retention a199f98a4716aa6315e08b6a71f97b90ea21214b197ed7b219b7515a77ba1235 |
| +internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; private immutable authorization, source/event identities and durable progress; collection deadline requires complete closed removal and known namespace, no earlier than parked time plus transport receipt retention; validates all-or-none persistent source and event proof before accepting progress 23633a2a4949c1de529f192bf19aab927b0218bc0ed8cdbc67a55463c22e15bd |
| 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 acquires these inputs under the host lock through the typed journal service 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; live edge peer reauthentication under namespace and host locks before census, inventory and final claim checks; root request entry refuses nil connections d3582f0760b4459721b3e6040cda738b105c1da19cc5d0746d04fea26e313b5c |
| @@ -75,7 +75,7 @@ internal/host/claimservingparkroot_linux.go validateServingRootHistory authority |
| 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 7ab31702b7dc1f909f0c3feb31042727737b908618992b5f3a51facc00480285 |
| +@coverage internal/host 8e632e791c936517445aa9719444af1e2f41dd698f82299b0b989c11a4879e43 |
| @coverage internal/hostbox eb1f4980ba609cc886ae4bd73aedbb6be3fb2eb104165bb1d8404da0814ed190 |
| @coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6 |
| internal/host/claimservinglegacy.go servingLegacyKey authority C,J4 rev3=PROPOSED-seed; box, host and incarnation derive private stop and consumption keys; never a caller path 93c10e40202ab23986b54a366edd7abfab9f8d942736305b5732e8741e96f3ab |
| @@ -88,8 +88,8 @@ internal/host/claimservinglegacy.go servingParkLedger.legacyUnlaunched authority |
| internal/host/claimservinglegacyroot_linux.go servingParkRoot.retainLegacyNoProcess authority C,J4 rev3=PROPOSED-seed; root coordinator result before publication; namespace and host guards, census, roster, fresh original claim and immutable retained stop; repeated preadmission witnesses tolerate only Gen, ETag and retry metadata changes file:Read@1210 5460530ae4367720515089e9fda3105aa1032b8282f4923bbf7f8af9600f4f63 |
| internal/host/claimservinglegacyroot_linux.go servingParkRoot.validateLegacyRootEvidence authority C,J4 rev3=PROPOSED-seed; retained no-process or stopped-admission evidence; exact predecessor fields with canonical recorded directory revision bounded by the current actor aa0dfe36c042ab65fd826e4b83eb81bfb9d03e6a54daafa828d5164492329d0d |
| internal/host/claimservinglegacyroot_linux.go servingParkRoot.parkLegacyServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh serving claim, root installation, consumed legacy identity and exact stop/empty evidence; collected consumption refuses recapture, while consumption saved before the first intent still resumes file:Read@3525 c192eda0fc53bc5211b02123ed7745e8e2b1b2fc53ad5fb3b0d11fe95fd9672c |
| -internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority C,J4 rev3=PROPOSED-seed; immutable directory and artifact identities from consumed source and retirement; mutable rename/delete progress is excluded 925e4f20aee042895a83ea3787f4482ae732b6c6bcd45d9d10226ff87bd37091 |
| -internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection file:Prepare@3449,file:Read@1774 099cf4772da0b33d2685e3d2b4de826923a256ba8e8dd576887df1d270c78abd |
| +internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority C,J4 rev3=PROPOSED-seed; immutable directory and artifact identities from consumed source and retirement; mutable rename/delete progress is excluded; source boot and persistent source-directory identity also constrain the immutable legacy source comparison af04f97f71570ea92099c78c613d252d1174864d54a5161318fcd9d5ecd958fa |
| +internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection; old-format replay preserves original consumption and source evidence without inferred handles file:Prepare@3772,file:Read@1774 c0cdd64d93a729abd8cf2a48606579516824c35181f977631de4a1a395a0b8c7 |
| internal/host/claimservingpark_records.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-identical root stop sets including targets, evidence timestamps and original receipt bytes c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88 |
| 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 |
| @@ -126,7 +126,7 @@ internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C, |
| internal/host/claimservingparkroot_linux.go servingParkRoot.withRootInstallation authority C,J4 rev3=PROPOSED-seed; fresh claim, root installation and directory inventory, eligible enrollment, paired tags, census gate and post-input claim reread under namespace then host guard; startup grants no new identity file:Read@2113 8060d3a2628f86257c3600c986242e0d4957ee918214a89655af828dd3ddf93b |
| internal/host/claimservingpark_linux.go servingParkFS.removeEvent authority C,J4 rev3=PROPOSED-seed; recorded current or displaced namespace and empty event; durable removal intent, descriptor-relative rmdir and fsync; ambiguous old-root absence refuses file:Readdirnames@790 66aa36321bc2bf3245daec35032783bb164cb21fe9b47620fbec71fd4b56cb89 |
| internal/host/claimservingpark_linux.go servingParkFS.removedEvent authority C,J4 rev3=PROPOSED-seed; absence within the exact recorded current or displaced root; root replacement and ambiguous pre-upgrade absence refuse d4100944fa7e41b83578839a91df3e546fa069f2c2558b9e8216b53fd329306d |
| -internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive collection deadline requires full completion and event removal; original authorization and retention deadline remain immutable ea0f918608527d73d259ca2c0947bea6a24485e1ac0777715282ae43b1fbb53d |
| +internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive collection deadline requires full completion and event removal; original authorization and retention deadline remain immutable; optional persistent event-directory handle recorded before rename 2bfdfe6cffe89e4a070c274463b65496eb2e17457f062fe6bbf6157ec1d14a62 |
| internal/host/claimservingpark_recovery.go const servingParkRecoveryLimit authority C,J4 rev3=PROPOSED-seed; fixed root-side processing limit of 128 retirement records per recovery page 6b949581334f55a7bae1b4579e56ac74810fc3042fdf568fbfa8e6b3c5a2ef11 |
| internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags 8fa87ebef4defae4fc1733528a76e0cc260711c2f5a664308e5368d69286d45b |
| internal/host/claimservingpark_recovery.go type servingParkRecoveryPage authority C,J4 rev3=PROPOSED-seed; private bounded recovery results, cursor and incomplete indicator; never a transport selector c2fb4445ed50b5237b72e8a5fff9b1b2f6c9dd1397473eeb2c78a6b8c2804b27 |
| @@ -134,7 +134,7 @@ internal/host/claimservingpark_recovery.go type servingParkRecoveryResult author |
| internal/host/claimservingpark_recovery.go servingParkLedger.recoveryIDs authority C,J4 rev3=PROPOSED-seed; private rootstate names; sorted bounded retirement page and continuation cursor; malformed record IDs retained for reporting 5b450cd0fb6ae6694bca2f9f75fe972c0d65681981a4caddde7e5c7269108782 |
| internal/host/claimservingpark_recovery.go servingParkLedger.recoverCompleted authority C,J4 rev3=PROPOSED-seed; private completed retirement, exact filesystem deletion and slot references; resume removal, clear completed pending reference and collect only obsolete tombstones after durable replay deadline a2a277c862ec65e8abaf8b93b7eba77a79e538a76d061fb080f37f6c0c6d48f4 |
| internal/host/claimservingpark_recovery.go servingParkRuntime.recoverPending authority C,J4 rev3=PROPOSED-seed; service lifetime cancellation and join; bounded startup results rendered only with indexed host/record/fixed-reason text 0aa3ef7edd0ac2c12b4221907ccd5927e7e3e03a4c2c0b77b669bc10cc49c241 |
| -internal/host/claimservinglegacy.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; compares original evidence while ignoring per-artifact progress and the separate collection flag 1524b2aae315e05734a666f80596bc341a007098a84fedeaf1e32451663d4fd4 |
| +internal/host/claimservinglegacy.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; compares original evidence while ignoring per-artifact progress and the separate collection flag; source boot and persistent directory identity must remain equal to the permanent legacy-consumption record dff50631bc0eac3586c220ea2a131cf233a35b20e3b3f5e8fe35904fcc43c722 |
| internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingPark authority C,J4 rev3=PROPOSED-seed; retained intent, exact current successor, independently rechecked installation, enrollment, receipt and local launch history; resumes original park and completes only existing paired tags; no retry settlement 0839c879792469648ceb62edce21319050351cf4db0a57c1a5220282afb7808f |
| internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingParks authority C,J4 rev3=PROPOSED-seed; private record page and per-record validation; continues past corrupt or refused hosts and reports incomplete enumeration; completed deletion separated from live host authority; first page recovers namespace under its lock using separate private parent evidence, without selecting a live host fb838aec596eaf1b26069f16006b5b2512284710c9bcebf8abfc65dbc9eb13cf |
| 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 |
| @@ -182,11 +182,25 @@ internal/host/claimservingboot_linux.go servingParkFS.checkBoot authority C,J4 r |
| internal/host/claimservingpark_linux.go type servingParkNamespace authority C,J4 rev3=PROPOSED-seed; durable parent and private-root identities with additive kernel boot scope; an old record is decodable but lacks current mutation authority; optional persistent parent/root proof captured at creation, never inferred for older records e5b695d1869a0565d199267dae6f16ace6090c57a4b3c4bdfe2871fbe81d46df |
| internal/host/claimservingpark_linux.go type servingParkFS authority C,J4 rev3=PROPOSED-seed; private filesystem dependencies including production kernel boot reader; requests cannot supply boot identity; descriptor-only persistent-handle reader, no open-by-handle operation 0a35b6ac9affb2c9be7577efe9081a54e7c6a74368a9d3502e193a6b29fdd2b2 |
| internal/host/claimservingnamespace_linux.go type servingParkParent authority C,J4 rev3=PROPOSED-seed; root-installation parent identity and additive boot scope; startup never infers an earlier boot from a matching mount number c58afd9dda038b4adb9ebf67fe0a42f1f3dd48f21d11b8f5660bad278e4dfb87 |
| -internal/host/claimservingboot_linux.go validServingParkBootID authority C,J4 rev3=PROPOSED-seed; canonical nonzero kernel boot UUID validation shared by live boot reads and historical topology inspection 76350dcd46b8c66e2e70945267537fb580e15b8da9c80fb158392d317973d3b2 |
| +internal/host/claimservingidentity.go validServingParkBootID authority C,J4 rev3=PROPOSED-seed; canonical nonzero kernel boot UUID validation shared by live boot reads and historical topology inspection 76350dcd46b8c66e2e70945267537fb580e15b8da9c80fb158392d317973d3b2 |
| internal/host/claimservingtopology_linux.go servingParkFS.captureTopology authority C,J4 rev3=PROPOSED-seed; current boot and pinned parent/root descriptors; persistent filesystem identities, handles, owner and mode; rereads descriptor identities and boot before returning creation evidence 169ff4183b9d65770c49aeaa341ad061ee38105e8cdea4ccfb5ee3f57079ec20 |
| internal/host/claimservingtopology_linux.go servingParkFS.inspectRebootTopology authority C,J4 rev3=PROPOSED-seed; private namespace and independently recorded parent, absent quarantine, current nosymfollow mount, persistent filesystem/handle identity and repeated path/permission checks; read-only observation grants no parking or deletion authority file:Read@491,file:Read@946 39d3d1b6e6791937f18f4064e0b523c708c457459adc27be7e1d5bb3870afbbe |
| internal/host/claimservingtopology_linux.go servingParkPersistentDir authority C,J4 rev3=PROPOSED-seed; descriptor-only Fstat, Fstatfs and NameToHandleAt with AT_EMPTY_PATH; bounded canonical handle and matching statx mount, mode and owner; no caller path, handle opening or permission widening file:Fstat@279,file:Fstatfs@375 2c60437509897dc3d7e861bf554a92cd5c338301b3bfbdee6b5d731ea9d3f7b8 |
| -internal/host/claimservingtopology_linux.go servingParkPersistentDirectory.valid authority C,J4 rev3=PROPOSED-seed; persistent handle format, bounded 1-128 byte handle, filesystem identity, directory type and non-writable group/other mode b161b8aa379bd9822825fb44718679df05d7d7b3a411f14cce2190c38f29a803 |
| -internal/host/claimservingtopology_linux.go type servingParkPersistentDirectory authority C,J4 rev3=PROPOSED-seed; root-recorded filesystem identity, opaque persistent directory handle and owner/mode, independent of numeric mount identity dced817ce21faf3ea261c82c6bf2b8036a74c14a266cd96b48618ce5bc746445 |
| +internal/host/claimservingidentity.go servingParkPersistentDirectory.valid authority C,J4 rev3=PROPOSED-seed; persistent handle format, bounded 1-128 byte handle, filesystem identity, directory type and non-writable group/other mode a40d3a371641b4b51616f86b60c9fd6506cd72c5667df940cfd981ccd3925191 |
| +internal/host/claimservingidentity.go type servingParkPersistentDirectory authority C,J4 rev3=PROPOSED-seed; root-recorded filesystem identity, opaque persistent directory handle and owner/mode, independent of numeric mount identity dced817ce21faf3ea261c82c6bf2b8036a74c14a266cd96b48618ce5bc746445 |
| internal/host/claimservingtopology_linux.go type servingParkRebootObservation authority C,J4 rev3=PROPOSED-seed; read-only old namespace and current boot/topology observation; no effect consumer or transport result accepts it e000b7d132c36a4979c5be443fc87996beafe2a8db320bf978ef1f026a09baa2 |
| internal/host/claimservingtopology_linux.go type servingParkTopology authority C,J4 rev3=PROPOSED-seed; persistent parent/root evidence attached only when root creates and records a new namespace 16a7fce2850bf204ddd67aa10876979be08a5d500a21ea3072e2fc5c1c369586 |
| +internal/host/claimservingidentity.go sameServingParkSource authority C,J4 rev3=PROPOSED-seed; immutable source versus fresh capture; old records compare original identity fields without acquiring new historical evidence 251d4c3de760b413dbbd3a0bdad03ff57560146e20794b9acb723cc142d046bf |
| +internal/host/claimservingidentity.go servingParkPersistentFile.valid authority C,J4 rev3=PROPOSED-seed; persistent regular-file type and opaque handle validation; no pathname or handle opening 6e071ea46888326a8a5040f486d42b07194c46f2b186fb163d53f60d45f147ba |
| +internal/host/claimservingidentity.go type servingParkPersistentFile authority C,J4 rev3=PROPOSED-seed; root-recorded regular-file filesystem handle, owner and mode, stored with immutable source evidence 003b704d58342ed25fd8c9cd557a69094115e0f22f70f35bb544e57da35c5ada |
| +internal/host/claimservingidentity.go validServingParkHandle authority C,J4 rev3=PROPOSED-seed; bounded canonical opaque handle, nonzero filesystem and handle type; pure validation shared by directory and regular-file records 48c0cbdeb5a2173629a1d7414819eea3894403b1f39241f5f49a9717e579466f |
| +internal/host/claimservingidentity.go validateServingParkPersistentRecord authority C,J4 rev3=PROPOSED-seed; source boot and directory identity, every present or absent file handle and prepared event proof; rejects partial evidence, preserves old record decoding bc7480d85f681f30580779f7d8fa3155d93509b804bd34212b56cc01e4eec69e |
| +internal/host/claimservingidentity_linux.go servingParkPersistentArtifact authority C,J4 rev3=PROPOSED-seed; no-follow O_PATH file descriptor, Fstatfs, Statx and NameToHandleAt with AT_EMPTY_PATH; name/descriptor device, inode, mount, type, links, allocation, ownership and mode checks; no tenant payload read file:Fstatfs@608,file:Openat@163,file:Statx@1062,file:Statx@347 b3fd1ed9332350bb19d0d34143b3c25ff44a39705207c0034f472690432575db |
| +internal/host/claimservingidentity_linux.go servingParkStatxMatches authority C,J4 rev3=PROPOSED-seed; recorded file identity versus descriptor or name statx metadata, including single regular-file link and allocated bytes 42ce5eb11aceab2c7f5d1999e91750d57ae668b0467a15b2c87953ca4aa17d63 |
| +internal/host/claimservingpark_records.go type servingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, device/inode/mount, type, link count, allocation and progress, with additive persistent file handle; optional persistent filesystem handle for the original file a5524de42a9f8514811326c297b304f9ab4f99b4650f6239895e9207725f30dc |
| +internal/host/claimservingpark_records.go type servingParkSource authority C,J4 rev3=PROPOSED-seed; immutable source directory and four artifacts; additive capture boot and persistent source identity; absent proof never gains inferred history; optional persistent source-directory handle and capture boot 5acbdb6ed1b1d3f2344ede87fc63652e8deea59e3105499e11ac6d465c1ac848 |
| +internal/host/claimservingreboot_linux.go servingParkFS.inspectRebootRecord authority C,J4 rev3=PROPOSED-seed; repeated read-only per-retirement inspection under namespace lock; unchanged historical record and topology; result is not mutation or agent authority baacf7e1df1d8fd4a09a5f6204597ab51b8765feca9c614ddc74d863a3cf9055 |
| +internal/host/claimservingreboot_linux.go servingParkFS.observeRebootRecord authority C,J4 rev3=PROPOSED-seed; private retirement record and persistent namespace evidence; exact source/event/file handles across boots, bounded event children, per-artifact rename/delete progress; completed parks do not consult live sources file:Readdirnames@2298 4de98aacfd52eff4b1e20395555e352ced86e55ef545d0013febb4b6cc9a3bf1 |
| +internal/host/claimservingreboot_linux.go servingParkFS.rebootDirectory authority C,J4 rev3=PROPOSED-seed; pinned descriptor device/inode, current mount and historical filesystem handle, owner and mode; no effects 2224be87510731990ca620abc0624a80675eca21228e561fa3e540d2ac8273c0 |
| +internal/host/claimservingreboot_linux.go servingParkRebootFileMatches authority C,J4 rev3=PROPOSED-seed; historical file handle and current mount compared to source/event observations; lost rename and unlink replies require original progress; no writes 180ec916768fd2d012e689199963586dabbaa77553cea9d806882d1eacb15a75 |
| +internal/host/claimservingreboot_linux.go type servingParkRebootRecord authority C,J4 rev3=PROPOSED-seed; read-only historical retirement and current source/event/namespace observations; has no effect or transport consumer 2752876f2e40f92a720c17c05e479dbfc7c4910392a1e6dc5ba4dc82b566763c |