carlos / platform Public

WIP files seed checkpoint 3 (not for merge): filesystem parking and durable retirement mechanisms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 95783aebf96a5c743f6165f31b7719497c6afb4b parent 3e86b74
8 files changed, +1951 −3
  • internal/host/claimservingpark_linux.go +535 −0
  • internal/host/claimservingpark_linux_test.go +708 −0
  • internal/host/claimservingpark_other.go +20 −0
  • internal/host/claimservingpark_records.go +491 −0
  • internal/host/claimservingparksource_linux.go +73 −0
  • internal/host/claimservingparksource_linux_test.go +83 −0
  • internal/provision/plan9_b8_census_test.go +3 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +38 −3

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	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	358.286s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.452s
ok  	github.com/carlosframework/platform/cmd/moveprobe	94.086s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.775s
ok  	github.com/carlosframework/platform/internal/activator	235.167s
ok  	github.com/carlosframework/platform/internal/agentsock	0.031s
ok  	github.com/carlosframework/platform/internal/alerting	0.011s
ok  	github.com/carlosframework/platform/internal/analytics	0.028s
ok  	github.com/carlosframework/platform/internal/appconfig	2.989s
ok  	github.com/carlosframework/platform/internal/apperr	0.120s
ok  	github.com/carlosframework/platform/internal/applog	0.424s
ok  	github.com/carlosframework/platform/internal/awsinv	0.014s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.015s
ok  	github.com/carlosframework/platform/internal/boxid	1.174s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/caps	0.030s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.780s
ok  	github.com/carlosframework/platform/internal/changelog	0.372s
ok  	github.com/carlosframework/platform/internal/clamd	0.062s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	368.602s
?   	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	6.248s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.011s
ok  	github.com/carlosframework/platform/internal/dbverify	0.015s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.008s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.332s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.007s
ok  	github.com/carlosframework/platform/internal/edge	28.199s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.019s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.143s
ok  	github.com/carlosframework/platform/internal/gate	1.770s
ok  	github.com/carlosframework/platform/internal/genselect	0.018s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.036s
ok  	github.com/carlosframework/platform/internal/host	132.609s
ok  	github.com/carlosframework/platform/internal/hostactors	0.953s
ok  	github.com/carlosframework/platform/internal/hostbox	3.246s
ok  	github.com/carlosframework/platform/internal/hostclaim	1.207s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	8.146s
ok  	github.com/carlosframework/platform/internal/hostfence	0.047s
ok  	github.com/carlosframework/platform/internal/hostfiles	11.818s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.030s
ok  	github.com/carlosframework/platform/internal/hostguard	1.123s
ok  	github.com/carlosframework/platform/internal/hostops	2.872s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	1.121s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.275s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.005s
ok  	github.com/carlosframework/platform/internal/instancectl	1.839s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.971s
ok  	github.com/carlosframework/platform/internal/instancefs	0.013s
ok  	github.com/carlosframework/platform/internal/instancekey	0.006s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.010s
ok  	github.com/carlosframework/platform/internal/layout	2.370s
ok  	github.com/carlosframework/platform/internal/lease	9.650s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.013s
ok  	github.com/carlosframework/platform/internal/ledger	0.031s
ok  	github.com/carlosframework/platform/internal/mail	0.326s
ok  	github.com/carlosframework/platform/internal/maintenance	0.143s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.888s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.179s
ok  	github.com/carlosframework/platform/internal/metrics	0.018s
ok  	github.com/carlosframework/platform/internal/moves	23.545s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.367s
ok  	github.com/carlosframework/platform/internal/objstore	0.348s
ok  	github.com/carlosframework/platform/internal/ops	0.222s
ok  	github.com/carlosframework/platform/internal/park	0.502s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	18.324s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.202s
ok  	github.com/carlosframework/platform/internal/relay	7.255s
ok  	github.com/carlosframework/platform/internal/release	23.130s
ok  	github.com/carlosframework/platform/internal/rollout	5.155s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.015s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.019s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.015s
ok  	github.com/carlosframework/platform/internal/safefile	0.006s
ok  	github.com/carlosframework/platform/internal/schedule	3.001s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	16.346s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.662s
ok  	github.com/carlosframework/platform/internal/status	0.104s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.260s
ok  	github.com/carlosframework/platform/internal/svcrole	0.005s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.009s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.010s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.892s
=== 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/14 07:03:38 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE536A5DBFF24A, 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.022s
minio contract: PASS
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go
new file mode 100644
index 000000000..ddb47bd02
--- /dev/null
+++ b/internal/host/claimservingpark_linux.go
@@ -0,0 +1,535 @@
+//go:build linux
+
+package host
+
+import (
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/instancefs"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+type servingParkNamespace struct {
+ Version int
+ Parent, Root servingArtifactDirectory
+}
+
+type servingParkFS struct {
+ data string
+ state rootstate.Store
+ owner uint32
+ mount func(string) (bool, error)
+ rename func(int, string, int, string, uint) error
+ sync func(int) error
+ unlink func(int, string, int) error
+}
+
+// Production construction always requires root and a nosymfollow mount. Tests
+// use the private fields to exercise filesystems without mount privileges.
+func newServingParkFS(data string, state rootstate.Store) *servingParkFS {
+ return &servingParkFS{data: data, state: state, owner: 0, mount: instancefs.MountNoSymfollow, rename: unix.Renameat2, sync: unix.Fsync, unlink: unix.Unlinkat}
+}
+
+func servingParkDirID(f *os.File) (servingArtifactDirectory, error) {
+ var st unix.Statx_t
+ if err := unix.Statx(int(f.Fd()), "", unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &st); err != nil {
+ return servingArtifactDirectory{}, err
+ }
+ if st.Mask&unix.STATX_MNT_ID == 0 || st.Mode&unix.S_IFMT != unix.S_IFDIR || st.Ino == 0 || st.Mnt_id == 0 {
+ return servingArtifactDirectory{}, hostbox.ErrChanged
+ }
+ return servingArtifactDirectory{Device: unix.Mkdev(st.Dev_major, st.Dev_minor), Inode: st.Ino, Mount: st.Mnt_id}, nil
+}
+
+func (f *servingParkFS) parent() (*os.File, servingArtifactDirectory, error) {
+ var zero servingArtifactDirectory
+ if f == nil || f.state == nil || f.mount == nil || f.rename == nil || f.sync == nil || f.unlink == nil || !filepath.IsAbs(f.data) || filepath.Clean(f.data) != f.data || uint32(os.Geteuid()) != f.owner {
+ return nil, zero, hostbox.ErrUnsupported
+ }
+ path := filepath.Join(f.data, "instances")
+ p, err := instancefs.OpenDirBeneath(path, ".")
+ if err != nil {
+ return nil, zero, err
+ }
+ id, err := servingParkDirID(p)
+ if err != nil {
+ p.Close()
+ return nil, zero, err
+ }
+ ok, err := f.mount(path)
+ if err != nil || !ok {
+ p.Close()
+ return nil, zero, hostbox.ErrChanged
+ }
+ // The pathname's effective mount must still name the pinned descriptor.
+ again, err := instancefs.OpenDirBeneath(path, ".")
+ if err != nil {
+ p.Close()
+ return nil, zero, err
+ }
+ next, err := servingParkDirID(again)
+ again.Close()
+ if err != nil || next != id {
+ p.Close()
+ return nil, zero, hostbox.ErrChanged
+ }
+ return p, id, nil
+}
+
+func servingParkOpenDir(parent *os.File, name string) (*os.File, error) {
+ if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") {
+ return nil, hostbox.ErrChanged
+ }
+ fd, err := unix.Openat(int(parent.Fd()), name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ return os.NewFile(uintptr(fd), name), nil
+}
+
+func (f *servingParkFS) privateDir(parent *os.File, name string) (*os.File, servingArtifactDirectory, error) {
+ var zero servingArtifactDirectory
+ d, err := servingParkOpenDir(parent, name)
+ if err != nil {
+ return nil, zero, err
+ }
+ var st unix.Stat_t
+ err = unix.Fstat(int(d.Fd()), &st)
+ if err != nil || st.Uid != f.owner || st.Mode&0077 != 0 {
+ d.Close()
+ return nil, zero, hostbox.ErrChanged
+ }
+ id, err := servingParkDirID(d)
+ if err != nil {
+ d.Close()
+ return nil, zero, err
+ }
+ return d, id, nil
+}
+
+// namespace refuses every unrecorded obstruction. Quarantine recovery must be
+// completed before a production caller can recover an unknown park root.
+func (f *servingParkFS) namespace() (*os.File, servingParkNamespace, error) {
+ var n servingParkNamespace
+ parent, id, err := f.parent()
+ if err != nil {
+ return nil, n, err
+ }
+ defer parent.Close()
+ b, err := f.state.Read("namespace.json")
+ if errors.Is(err, os.ErrNotExist) {
+ // EEXIST is deliberately not adoption authority for a service-owned name.
+ if err = unix.Mkdirat(int(parent.Fd()), ".serving-parked", 0700); err != nil {
+ return nil, n, err
+ }
+ d, rid, e := f.privateDir(parent, ".serving-parked")
+ if e != nil {
+ return nil, n, e
+ }
+ if rid.Device != id.Device || rid.Mount != id.Mount {
+ d.Close()
+ return nil, n, hostbox.ErrChanged
+ }
+ n = servingParkNamespace{Version: 1, Parent: id, Root: rid}
+ if e = f.sync(int(parent.Fd())); e == nil {
+ l := servingParkLedger{state: f.state}
+ e = l.write("namespace.json", n)
+ }
+ if e != nil {
+ d.Close()
+ return nil, n, e
+ }
+ return d, n, nil
+ }
+ if err != nil {
+ return nil, n, err
+ }
+ if servingParkDecode(b, &n) != nil || n.Version != 1 || n.Parent != id {
+ return nil, n, hostbox.ErrChanged
+ }
+ d, rid, err := f.privateDir(parent, ".serving-parked")
+ if err != nil {
+ return nil, n, err
+ }
+ if rid != n.Root || rid.Device != id.Device || rid.Mount != id.Mount {
+ d.Close()
+ return nil, n, hostbox.ErrChanged
+ }
+ return d, n, nil
+}
+
+func (f *servingParkFS) source(a servingParkAuthorization) (*os.File, servingArtifactDirectory, error) {
+ var zero servingArtifactDirectory
+ host := a.Successor.Host
+ if host == "" || host == "." || host == ".." || strings.ContainsAny(host, "/\\\x00") || len(host)+7 > 255 {
+ return nil, zero, hostbox.ErrChanged
+ }
+ p, pid, err := f.parent()
+ if err != nil {
+ return nil, zero, err
+ }
+ defer p.Close()
+ d, err := servingParkOpenDir(p, provision.InstanceID(host))
+ if err != nil {
+ return nil, zero, err
+ }
+ id, err := servingParkDirID(d)
+ if err != nil || id != a.Directory || id.Device != pid.Device || id.Mount != pid.Mount {
+ d.Close()
+ return nil, zero, hostbox.ErrChanged
+ }
+ return d, pid, nil
+}
+
+func servingParkStat(parent *os.File, name string) (servingParkFile, error) {
+ out := servingParkFile{Name: name}
+ var st unix.Statx_t
+ err := unix.Statx(int(parent.Fd()), name, unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &st)
+ if errors.Is(err, unix.ENOENT) {
+ return out, nil
+ }
+ if err != nil {
+ return out, err
+ }
+ if st.Mask&unix.STATX_MNT_ID == 0 || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 || st.Ino == 0 {
+ return out, hostbox.ErrChanged
+ }
+ // Allocated bytes are the upper bound reclaimable by removing this link;
+ // pressure callers must still remeasure f_bavail after deleting it.
+ if st.Blocks > ^uint64(0)/512 {
+ 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
+}
+
+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
+}
+
+func (f *servingParkFS) Capture(a servingParkAuthorization) (servingParkSource, error) {
+ var out servingParkSource
+ source, parent, err := f.source(a)
+ if err != nil {
+ return out, err
+ }
+ defer source.Close()
+ out.Directory, out.Parent = a.Directory, parent
+ for i, suffix := range []string{"", ".gen", "-wal", "-shm"} {
+ file, err := servingParkStat(source, a.Successor.Host+".db"+suffix)
+ if err != nil {
+ return out, err
+ }
+ if file.Present && (file.Device != parent.Device || file.Mount != parent.Mount) {
+ return out, hostbox.ErrChanged
+ }
+ out.Files[i] = file
+ }
+ marker := out.Files[1]
+ if marker.Present {
+ if a.Legacy {
+ return out, hostbox.ErrChanged
+ }
+ fd, err := unix.Openat(int(source.Fd()), marker.Name, unix.O_RDONLY|unix.O_NONBLOCK|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return out, err
+ }
+ mf := os.NewFile(uintptr(fd), marker.Name)
+ var st unix.Stat_t
+ if err = unix.Fstat(fd, &st); err != nil || uint64(st.Dev) != marker.Device || st.Ino != marker.Inode || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 {
+ mf.Close()
+ return out, hostbox.ErrChanged
+ }
+ body, err := io.ReadAll(io.LimitReader(mf, 4097))
+ mf.Close()
+ if err != nil || len(body) > 4096 || strings.TrimSpace(string(body)) != a.Predecessor.Generation {
+ return out, hostbox.ErrChanged
+ }
+ again, err := servingParkStat(source, marker.Name)
+ if err != nil || !sameServingParkFile(marker, again) {
+ return out, hostbox.ErrChanged
+ }
+ }
+ return out, nil
+}
+
+func (f *servingParkFS) event(r *servingParkRecord) (*os.File, *os.File, error) {
+ root, n, err := f.namespace()
+ if err != nil {
+ return nil, nil, err
+ }
+ if n.Parent != r.Source.Parent {
+ root.Close()
+ return nil, nil, hostbox.ErrChanged
+ }
+ event, id, err := f.privateDir(root, r.Authorization.ID)
+ if err != nil {
+ root.Close()
+ return nil, nil, err
+ }
+ if !r.Prepared || id != r.Event || id.Device != n.Root.Device || id.Mount != n.Root.Mount {
+ root.Close()
+ event.Close()
+ return nil, nil, hostbox.ErrChanged
+ }
+ return root, event, nil
+}
+
+func (f *servingParkFS) Prepare(r *servingParkRecord, save func() error) error {
+ if r.Prepared {
+ root, event, err := f.event(r)
+ if err != nil {
+ return err
+ }
+ defer root.Close()
+ defer event.Close()
+ return f.sync(int(root.Fd()))
+ }
+ root, n, err := f.namespace()
+ if err != nil {
+ return err
+ }
+ defer root.Close()
+ if n.Parent != r.Source.Parent {
+ return hostbox.ErrChanged
+ }
+ if err = unix.Mkdirat(int(root.Fd()), r.Authorization.ID, 0700); err != nil && !errors.Is(err, unix.EEXIST) {
+ return err
+ }
+ event, id, err := f.privateDir(root, r.Authorization.ID)
+ if err != nil {
+ return err
+ }
+ defer event.Close()
+ if id.Device != n.Root.Device || id.Mount != n.Root.Mount {
+ return hostbox.ErrChanged
+ }
+ // An event exists only under a recorded root-private namespace and a durable
+ // intent. Before its identity is recorded it must be entirely empty.
+ names, err := event.Readdirnames(1)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return err
+ }
+ if len(names) != 0 {
+ return hostbox.ErrChanged
+ }
+ if err = f.sync(int(root.Fd())); err != nil {
+ return err
+ }
+ r.Event, r.Prepared = id, true
+ return save()
+}
+
+func (f *servingParkFS) Move(r *servingParkRecord, save func() error) error {
+ root, event, err := f.event(r)
+ if err != nil {
+ return err
+ }
+ defer root.Close()
+ defer event.Close()
+ source, parent, err := f.source(r.Authorization)
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+ if parent != r.Source.Parent {
+ return hostbox.ErrChanged
+ }
+ // Check the complete set before moving any name.
+ for _, recorded := range r.Source.Files {
+ src, e := servingParkStat(source, recorded.Name)
+ if e != nil {
+ return e
+ }
+ dst, e := servingParkStat(event, recorded.Name)
+ if e != nil {
+ return e
+ }
+ if !recorded.Present {
+ if src.Present || dst.Present {
+ return hostbox.ErrChanged
+ }
+ } else if recorded.Moved {
+ if src.Present || !sameServingParkFile(recorded, dst) {
+ return hostbox.ErrChanged
+ }
+ } else if !((sameServingParkFile(recorded, src) && !dst.Present) || (!src.Present && sameServingParkFile(recorded, dst))) {
+ return hostbox.ErrChanged
+ }
+ }
+ for i := range r.Source.Files {
+ item := &r.Source.Files[i]
+ if !item.Present {
+ continue
+ }
+ src, e := servingParkStat(source, item.Name)
+ if e != nil {
+ return e
+ }
+ if src.Present {
+ if item.Moved || !sameServingParkFile(*item, src) {
+ return hostbox.ErrChanged
+ }
+ if e = f.rename(int(source.Fd()), item.Name, int(event.Fd()), item.Name, unix.RENAME_NOREPLACE); e != nil {
+ return e
+ }
+ }
+ // A replacement raced into rename may be retained here, never deleted.
+ dst, e := servingParkStat(event, item.Name)
+ if e != nil || !sameServingParkFile(*item, dst) {
+ return hostbox.ErrChanged
+ }
+ if e = f.sync(int(source.Fd())); e != nil {
+ return e
+ }
+ if e = f.sync(int(event.Fd())); e != nil {
+ return e
+ }
+ if !item.Moved {
+ item.Moved = true
+ if e = save(); e != nil {
+ return e
+ }
+ }
+ }
+ return nil
+}
+
+func (f *servingParkFS) Delete(r *servingParkRecord, save func() error) error {
+ root, event, err := f.event(r)
+ if err != nil {
+ return err
+ }
+ defer root.Close()
+ defer event.Close()
+ // Validate all children before unlinking any one of them. There is no
+ // recursive deletion and no pathname derived from a registry row.
+ names, err := event.Readdirnames(-1)
+ if err != nil {
+ return err
+ }
+ expected := make(map[string]servingParkFile)
+ for _, item := range r.Source.Files {
+ if item.Present {
+ expected[item.Name] = item
+ }
+ }
+ for _, name := range names {
+ item, ok := expected[name]
+ if !ok || item.Deleted {
+ return hostbox.ErrChanged
+ }
+ actual, e := servingParkStat(event, name)
+ if e != nil || !sameServingParkFile(item, actual) {
+ return hostbox.ErrChanged
+ }
+ }
+ for _, item := range r.Source.Files {
+ if !item.Present {
+ continue
+ }
+ actual, e := servingParkStat(event, item.Name)
+ if e != nil {
+ return e
+ }
+ if item.Deleted && actual.Present || !actual.Present && !item.DeleteIntent || actual.Present && !sameServingParkFile(item, actual) {
+ return hostbox.ErrChanged
+ }
+ }
+ for i := range r.Source.Files {
+ item := &r.Source.Files[i]
+ if !item.Present {
+ continue
+ }
+ actual, e := servingParkStat(event, item.Name)
+ if e != nil {
+ return e
+ }
+ if item.Deleted {
+ if actual.Present {
+ return hostbox.ErrChanged
+ }
+ continue
+ }
+ if !actual.Present && !item.DeleteIntent {
+ return hostbox.ErrChanged
+ }
+ if actual.Present && !sameServingParkFile(*item, actual) {
+ return hostbox.ErrChanged
+ }
+ if !item.DeleteIntent {
+ item.DeleteIntent = true
+ if e = save(); e != nil {
+ return e
+ }
+ }
+ if actual.Present {
+ if e = f.unlink(int(event.Fd()), item.Name, 0); e != nil {
+ return e
+ }
+ }
+ if e = f.sync(int(event.Fd())); e != nil {
+ return e
+ }
+ item.Deleted = true
+ if e = save(); e != nil {
+ return e
+ }
+ }
+ // Keep the recorded empty event until tombstone cleanup is implemented.
+ // It owns no database blocks and cannot be reused by another retirement.
+ return nil
+}
+
+func (f *servingParkFS) CheckParked(r *servingParkRecord) error {
+ root, event, err := f.event(r)
+ if err != nil {
+ return err
+ }
+ defer root.Close()
+ defer event.Close()
+ source, parent, err := f.source(r.Authorization)
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+ if parent != r.Source.Parent {
+ return hostbox.ErrChanged
+ }
+ expected := make(map[string]bool)
+ for _, item := range r.Source.Files {
+ src, e := servingParkStat(source, item.Name)
+ if e != nil || src.Present {
+ return hostbox.ErrChanged
+ }
+ dst, e := servingParkStat(event, item.Name)
+ if e != nil {
+ return e
+ }
+ if item.Present && !item.Deleted {
+ if !sameServingParkFile(item, dst) {
+ return hostbox.ErrChanged
+ }
+ expected[item.Name] = true
+ } else if dst.Present {
+ return hostbox.ErrChanged
+ }
+ }
+ names, err := event.Readdirnames(-1)
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ if !expected[name] {
+ return hostbox.ErrChanged
+ }
+ }
+ return nil
+}
diff --git a/internal/host/claimservingpark_linux_test.go b/internal/host/claimservingpark_linux_test.go
new file mode 100644
index 000000000..3af4ca23d
--- /dev/null
+++ b/internal/host/claimservingpark_linux_test.go
@@ -0,0 +1,708 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+// This fixture exercises the durable driver, with explicit seams for root UID,
+// nosymfollow and the outer claim/host-guard authorization. It is not a
+// privileged mount or production transport witness.
+type servingParkDiskFixture struct {
+ t *testing.T
+ ledger *servingParkLedger
+ fs *servingParkFS
+ disk *rollbackDisk
+ data string
+ now time.Time
+ base servingArtifactIdentity
+ target hostguard.FenceTarget
+}
+
+func newServingParkDiskFixture(t *testing.T) *servingParkDiskFixture {
+ t.Helper()
+ rf, _, _, in := servingRetirementFixture(t)
+ admitted := servingAdmit(t, rf, &in, "G")
+ receipt := servingStop(t, rf, admitted)
+ base, err := servingWriterIdentity(admitted, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ disk := &rollbackDisk{dir: t.TempDir()}
+ fs := newServingParkFS(rf.guard.DataDir, disk)
+ fs.owner = uint32(os.Geteuid())
+ fs.mount = func(string) (bool, error) { return true, nil }
+ f := &servingParkDiskFixture{t: t, fs: fs, disk: disk, data: rf.guard.DataDir, now: time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC), base: base, target: receipt.Target}
+ f.ledger = &servingParkLedger{state: disk, fs: fs, journal: rf.journal, now: func() time.Time { return f.now }}
+ return f
+}
+
+func (f *servingParkDiskFixture) evidence(n uint64, legacy bool, state string) (servingParkAuthorization, []hostguard.FenceReceipt) {
+ t := f.t
+ t.Helper()
+ prior, next := f.base, f.base
+ prior.Stamp, prior.Session, prior.Generation = n, n, fmt.Sprintf("G%d", n)
+ next.Stamp, next.Session, next.Generation = n+1, n+1, fmt.Sprintf("G%d", n+1)
+ target := f.target
+ target.Stamp, target.Session, target.FenceID = n, n, fmt.Sprintf("stop-%d", n)
+ s := StopReceipt{V: 1, ID: target.FenceID, Kind: StopReceiptStopped, Region: target.Region, ClaimETag: target.ClaimETag, ClaimGen: target.ClaimGen, FenceID: target.FenceID, Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, DirectoryRevision: target.DirectoryRevision, EvidenceAt: f.now}
+ receipts := []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(s), EvidenceAt: f.now}}
+ if state != "missing" && !legacy {
+ if err := f.ledger.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "controller-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil {
+ t.Fatal(err)
+ }
+ if state != "prelaunch" {
+ res := hostbox.LaunchReservation{ID: fmt.Sprintf("%d:main", n), Role: "main", InvocationID: fmt.Sprintf("invocation-%d", n), Inputs: "inputs", Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}}
+ if err := f.ledger.journal.IntendUnit(t.Context(), res); err != nil {
+ t.Fatal(err)
+ }
+ if state == "started" {
+ group := hostbox.LaunchGroup{RelativePath: res.Group.RelativePath, Device: 1, Inode: 4, InvocationID: fmt.Sprintf("unit-%d", n)}
+ if err := f.ledger.journal.StartedUnit(t.Context(), res, group, 99, "tenant-start"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if state != "intended" {
+ if err := f.ledger.journal.SettleUnit(t.Context(), target, res.ID); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ }
+ source, err := os.Open(filepath.Dir(provision.NestedDBPath(f.data, next.Host)))
+ if err != nil {
+ t.Fatal(err)
+ }
+ id, err := servingParkDirID(source)
+ source.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ a := servingParkAuthorization{TagsFrom: n, Predecessor: prior, OriginalSuccessor: next, Successor: next, Directory: id, Legacy: legacy}
+ if legacy {
+ a.Predecessor = servingArtifactIdentity{}
+ a.TagsFrom = 0
+ }
+ a.ID = servingRetirementID(a.Predecessor, next, legacy)
+ return a, receipts
+}
+func (f *servingParkDiskFixture) put(suffix, body string) {
+ f.t.Helper()
+ if err := os.WriteFile(provision.NestedDBPath(f.data, f.base.Host)+suffix, []byte(body), 0600); err != nil {
+ f.t.Fatal(err)
+ }
+}
+func (f *servingParkDiskFixture) park(a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) {
+ var r servingParkRecord
+ err := f.ledger.withNamespace(f.t.Context(), func() error { var err error; r, err = f.ledger.parkLocked(f.t.Context(), a, receipts); return err })
+ return r, err
+}
+func (f *servingParkDiskFixture) complete(a servingParkAuthorization) {
+ f.t.Helper()
+ if err := f.ledger.withNamespace(f.t.Context(), func() error { return f.ledger.completeLocked(f.t.Context(), a.ID, a.Successor) }); err != nil {
+ f.t.Fatal(err)
+ }
+}
+func (f *servingParkDiskFixture) path(a servingParkAuthorization, suffix string) string {
+ return filepath.Join(f.data, "instances", ".serving-parked", a.ID, a.Successor.Host+".db"+suffix)
+}
+func (f *servingParkDiskFixture) retained(a servingParkAuthorization, body string, deadline time.Time) {
+ f.t.Helper()
+ slot, err := f.ledger.slot(a)
+ if err != nil || slot.ID != a.ID {
+ f.t.Fatalf("park owner changed: %+v %v", slot, err)
+ }
+ b, err := os.ReadFile(f.path(a, ""))
+ if err != nil || string(b) != body {
+ f.t.Fatalf("retained database: %q %v", b, err)
+ }
+ r, err := f.ledger.read(a.ID)
+ if err != nil || !r.ReapAfter.Equal(deadline) {
+ f.t.Fatalf("park deadline changed: %v %v", r.ReapAfter, err)
+ }
+}
+
+func TestServingParkLedgerKeepsLastStartedDatabase(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy writes")
+ f.put("-wal", "legacy WAL")
+ f.put("-shm", "legacy SHM")
+ legacy, stops := f.evidence(10, true, "missing")
+ parked, err := f.park(legacy, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(legacy)
+ for _, suffix := range []string{"-wal", "-shm"} {
+ if b, e := os.ReadFile(f.path(legacy, suffix)); e != nil || len(b) == 0 {
+ t.Fatalf("companion not preserved: %s %v", suffix, e)
+ }
+ }
+ for n := uint64(11); n < 179; n++ {
+ f.now = f.now.Add(time.Hour)
+ // Alternate a failed restore with no database and a successful restore
+ // whose tenant never started. Neither owns the retention slot.
+ if n%2 == 0 {
+ f.put("", "unlaunched baseline")
+ }
+ f.put(".gen", fmt.Sprintf("G%d", n))
+ f.put("-wal", "companion")
+ next, receipts := f.evidence(n, false, "prelaunch")
+ r, e := f.park(next, receipts)
+ if e != nil {
+ t.Fatalf("retirement %d: %v", n, e)
+ }
+ if !r.Closed || r.Disposition == servingParkRetain {
+ t.Fatalf("unlaunched source retained: %+v", r)
+ }
+ f.complete(next)
+ f.retained(legacy, "legacy writes", parked.ReapAfter)
+ }
+ f.put("", "tenant writes")
+ next, receipts := f.evidence(179, false, "started")
+ r, err := f.park(next, receipts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(next)
+ if _, err := os.Stat(f.path(legacy, "")); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("legacy not replaced: %v", err)
+ }
+ f.retained(next, "tenant writes", r.ReapAfter)
+ // A tombstoned replay cannot replace the current slot or reset its clock.
+ if _, err := f.park(legacy, stops); err != nil {
+ t.Fatal(err)
+ }
+ f.complete(legacy)
+ f.retained(next, "tenant writes", r.ReapAfter)
+}
+
+func TestServingParkLedgerAmbiguousLaunchPreservesBothCopies(t *testing.T) {
+ for _, state := range []string{"missing", "intended", "settled-unobserved"} {
+ t.Run(state, func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ legacy, stops := f.evidence(10, true, "missing")
+ park, err := f.park(legacy, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(legacy)
+ f.put("", "uncertain tenant writes")
+ next, receipts := f.evidence(11, false, state)
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("ambiguous launch accepted")
+ }
+ f.retained(legacy, "legacy", park.ReapAfter)
+ b, err := os.ReadFile(provision.NestedDBPath(f.data, f.base.Host))
+ if err != nil || string(b) != "uncertain tenant writes" {
+ t.Fatalf("source changed: %q %v", b, err)
+ }
+ })
+ }
+}
+
+func TestServingParkLedgerExpiryAndIdentity(t *testing.T) {
+ for _, attack := range []string{"none", "inode", "hardlink", "symlink", "unknown child", "event", "root", "parent", "missing", "mount"} {
+ t.Run(attack, func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "parked")
+ a, stops := f.evidence(10, true, "missing")
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ f.now = r.ReapAfter.Add(-time.Nanosecond)
+ reap := func() error {
+ return f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) })
+ }
+ if err := reap(); err == nil {
+ t.Fatal("reaped before deadline")
+ }
+ f.now = r.ParkedAt.Add(-time.Hour)
+ if err := reap(); err == nil {
+ t.Fatal("clock rollback reaped park")
+ }
+ f.now = r.ReapAfter
+ path := f.path(a, "")
+ victim := filepath.Join(t.TempDir(), "victim")
+ if err := os.WriteFile(victim, []byte("victim"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ switch attack {
+ case "inode":
+ if err := os.Rename(path, path+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(path+".saved", filepath.Join(t.TempDir(), "original")); err != nil {
+ t.Fatal(err)
+ }
+ case "hardlink":
+ if err := os.Link(path, path+".link"); err != nil {
+ t.Fatal(err)
+ }
+ case "symlink":
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(victim, path); err != nil {
+ t.Fatal(err)
+ }
+ case "unknown child":
+ if err := os.WriteFile(path+".unknown", []byte("keep"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "event", "root", "parent":
+ dir := filepath.Dir(path)
+ if attack == "root" {
+ dir = filepath.Dir(dir)
+ }
+ if attack == "parent" {
+ dir = filepath.Dir(filepath.Dir(dir))
+ }
+ if err := os.Rename(dir, dir+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ case "missing":
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ case "mount":
+ f.fs.mount = func(string) (bool, error) { return false, nil }
+ }
+ err = reap()
+ if attack == "none" {
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, e := os.Stat(path); !errors.Is(e, os.ErrNotExist) {
+ t.Fatalf("not reaped: %v", e)
+ }
+ } else if err == nil {
+ t.Fatal("unsafe reaping accepted")
+ }
+ if b, e := os.ReadFile(victim); e != nil || string(b) != "victim" {
+ t.Fatalf("victim changed: %q %v", b, e)
+ }
+ })
+ }
+}
+
+func TestServingParkLedgerNoReplaceAndCrossMountRefusal(t *testing.T) {
+ for _, failure := range []error{unix.EXDEV, unix.EEXIST} {
+ t.Run(failure.Error(), func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "source")
+ a, stops := f.evidence(10, true, "missing")
+ f.fs.rename = func(int, string, int, string, uint) error { return failure }
+ if _, err := f.park(a, stops); !errors.Is(err, failure) {
+ t.Fatalf("rename result: %v", err)
+ }
+ b, err := os.ReadFile(provision.NestedDBPath(f.data, f.base.Host))
+ if err != nil || string(b) != "source" {
+ t.Fatalf("rename fallback touched source: %q %v", b, err)
+ }
+ f.fs.rename = unix.Renameat2
+ if _, err := f.park(a, stops); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
+
+func TestServingParkLedgerLostRenameAndDeleteReplies(t *testing.T) {
+ for _, effect := range []string{"rename", "sync", "unlink"} {
+ for boundary := 1; boundary <= 4; boundary++ {
+ t.Run(fmt.Sprintf("%s-%d", effect, boundary), func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ for _, suffix := range []string{"", "-wal", "-shm"} {
+ f.put(suffix, "source"+suffix)
+ }
+ a, stops := f.evidence(10, true, "missing")
+ calls := 0
+ fail := func(err error) error {
+ if err != nil {
+ return err
+ }
+ calls++
+ if calls == boundary {
+ return os.ErrPermission
+ }
+ return nil
+ }
+ if effect == "rename" {
+ f.fs.rename = func(a int, b string, c int, d string, e uint) error { return fail(unix.Renameat2(a, b, c, d, e)) }
+ }
+ if effect == "sync" {
+ f.fs.sync = func(fd int) error { return fail(unix.Fsync(fd)) }
+ }
+ _, _ = f.park(a, stops)
+ f.fs.rename, f.fs.sync = unix.Renameat2, unix.Fsync
+ // A failure before namespace.json exists is deliberately an obstruction;
+ // quarantine recovery (still unbound) must resolve it, never adoption.
+ if _, err := f.disk.Read("namespace.json"); errors.Is(err, os.ErrNotExist) {
+ if _, err := f.park(a, stops); err == nil {
+ t.Fatal("unrecorded namespace adopted")
+ }
+ return
+ }
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ for _, suffix := range []string{"", "-wal", "-shm"} {
+ if b, e := os.ReadFile(f.path(a, suffix)); e != nil || string(b) != "source"+suffix {
+ t.Fatalf("parked bytes: %q %v", b, e)
+ }
+ }
+ f.now = r.ReapAfter
+ if effect == "unlink" {
+ f.fs.unlink = func(fd int, n string, flags int) error { return fail(unix.Unlinkat(fd, n, flags)) }
+ }
+ _ = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) })
+ f.fs.unlink = unix.Unlinkat
+ if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+ }
+}
+
+func TestServingParkLedgerPendingCannotBeReplaced(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ a, stops := f.evidence(10, true, "missing")
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "new tenant writes")
+ next, receipts := f.evidence(11, false, "started")
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("pending park replaced")
+ }
+ f.retained(a, "legacy", r.ReapAfter)
+ if err := os.Remove(provision.NestedDBPath(f.data, f.base.Host)); err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ f.put("", "new tenant writes")
+ if _, err := f.park(next, receipts); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestServingParkLedgerCancelIsEffectFree(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ ctx, cancel := context.WithCancel(t.Context())
+ cancel()
+ called := false
+ if err := f.ledger.withNamespace(ctx, func() error { called = true; return nil }); !errors.Is(err, context.Canceled) || called {
+ t.Fatalf("cancel: %v, called=%v", err, called)
+ }
+}
+
+func TestServingParkLedgerDiscardResumesAfterUnlink(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ old, stops := f.evidence(10, true, "missing")
+ r, err := f.park(old, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(old)
+ f.put("", "unlaunched")
+ next, receipts := f.evidence(11, false, "prelaunch")
+ f.fs.unlink = func(fd int, n string, flags int) error {
+ if err := unix.Unlinkat(fd, n, flags); err != nil {
+ return err
+ }
+ return os.ErrPermission
+ }
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("lost unlink reply succeeded")
+ }
+ f.retained(old, "legacy", r.ReapAfter)
+ f.fs.unlink = unix.Unlinkat
+ if _, err := f.park(next, receipts); err != nil {
+ t.Fatal(err)
+ }
+ f.complete(next)
+ f.retained(old, "legacy", r.ReapAfter)
+}
+
+func TestServingParkLedgerSourceReplacementDoesNotDeleteOldSlot(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ old, stops := f.evidence(10, true, "missing")
+ r, err := f.park(old, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(old)
+ f.put("", "served")
+ next, receipts := f.evidence(11, false, "started")
+ // Interrupt just after the new source was captured and its intent saved.
+ f.disk.afterWrite = func(name string, _ []byte) {
+ if name == "retirement-"+next.ID+".json" {
+ panic("crash")
+ }
+ }
+ func() {
+ defer func() {
+ if recover() == nil {
+ t.Fatal("crash not reached")
+ }
+ }()
+ _, _ = f.park(next, receipts)
+ }()
+ f.disk.afterWrite = nil
+ source := provision.NestedDBPath(f.data, f.base.Host)
+ if err := os.Rename(source, source+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "replacement")
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("replaced source accepted")
+ }
+ f.retained(old, "legacy", r.ReapAfter)
+}
+
+func TestServingParkLedgerChecksAllArtifactsBeforeDeletion(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "database")
+ f.put("-shm", "shared memory")
+ a, stops := f.evidence(10, true, "missing")
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ if err := os.Remove(f.path(a, "-shm")); err != nil {
+ t.Fatal(err)
+ }
+ f.now = r.ReapAfter
+ if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }); err == nil {
+ t.Fatal("unrecorded absence accepted")
+ }
+ if b, e := os.ReadFile(f.path(a, "")); e != nil || string(b) != "database" {
+ t.Fatalf("partial validation deleted database: %q %v", b, e)
+ }
+}
+
+func TestServingParkLedgerCompletionRechecksSource(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ a, stops := f.evidence(10, true, "missing")
+ _, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "replacement")
+ if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.completeLocked(t.Context(), a.ID, a.Successor) }); err == nil {
+ t.Fatal("completed with replacement source")
+ }
+ r, err := f.ledger.read(a.ID)
+ if err != nil || r.Complete {
+ t.Fatalf("completion persisted: %v %v", r.Complete, err)
+ }
+}
+
+func TestServingParkLedgerLegacyConsumptionSurvivesReaping(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ a, stops := f.evidence(10, true, "missing")
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ f.now = r.ReapAfter
+ if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }); err != nil {
+ t.Fatal(err)
+ }
+ // Even removal of the disposable retirement tombstone does not reauthorize
+ // a different legacy retirement for this installation/incarnation.
+ if err := f.disk.Remove("retirement-" + a.ID + ".json"); err != nil {
+ t.Fatal(err)
+ }
+ slot, err := f.ledger.slot(a)
+ if err == nil {
+ t.Fatal("slot accepted missing tombstone")
+ }
+ // Model future slot/tombstone GC; the durable consumed-source record stays.
+ slot = servingParkSlot{Version: 1, Box: a.Successor.Box, Host: a.Successor.Host}
+ if err := f.ledger.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "restored successor")
+ next, receipts := f.evidence(11, true, "missing")
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("successor parked as legacy")
+ }
+}
+
+type servingParkFaultStore struct {
+ rootstate.Store
+ count, failAt int
+ before bool
+}
+
+func (s *servingParkFaultStore) Write(name string, body []byte) error {
+ s.count++
+ if s.count == s.failAt && s.before {
+ return os.ErrPermission
+ }
+ if err := s.Store.Write(name, body); err != nil {
+ return err
+ }
+ if s.count == s.failAt {
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func TestServingParkLedgerDurableWriteFailures(t *testing.T) {
+ for _, before := range []bool{false, true} {
+ for boundary := 1; boundary <= 24; boundary++ {
+ t.Run(fmt.Sprintf("before-%t-boundary-%d", before, boundary), func(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "database")
+ f.put("-wal", "wal")
+ f.put("-shm", "shm")
+ a, stops := f.evidence(10, true, "missing")
+ faults := &servingParkFaultStore{Store: f.disk, failAt: boundary, before: before}
+ f.ledger.state, f.fs.state = faults, faults
+ r, err := f.park(a, stops)
+ if err == nil {
+ err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.completeLocked(t.Context(), a.ID, a.Successor) })
+ }
+ if err == nil {
+ f.now = r.ReapAfter
+ err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) })
+ }
+ f.ledger.state, f.fs.state = f.disk, f.disk
+ if _, readErr := f.disk.Read("namespace.json"); os.IsNotExist(readErr) {
+ if _, statErr := os.Stat(filepath.Join(f.data, "instances", ".serving-parked")); statErr == nil {
+ if _, err := f.park(a, stops); err == nil {
+ t.Fatal("unknown root adopted after write failure")
+ }
+ return
+ }
+ }
+ r, err = f.park(a, stops)
+ if err != nil {
+ t.Fatalf("park replay: %v", err)
+ }
+ f.complete(a)
+ f.now = r.ReapAfter
+ if err := f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) }); err != nil {
+ t.Fatalf("reap replay: %v", err)
+ }
+ got, err := f.ledger.read(a.ID)
+ if err != nil || !got.Closed || !got.Complete {
+ t.Fatalf("unfinished replay: %+v %v", got, err)
+ }
+ })
+ }
+ }
+}
+
+func TestServingParkLedgerSlotsAcrossIncarnationsAndHosts(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "first incarnation")
+ old, stops := f.evidence(10, true, "missing")
+ _, err := f.park(old, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(old)
+ f.base.Incarnation, f.target.Incarnation = "next-incarnation", "next-incarnation"
+ f.put("", "next incarnation")
+ next, receipts := f.evidence(11, false, "started")
+ r, err := f.park(next, receipts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(next)
+ if _, err := os.Stat(f.path(old, "")); !os.IsNotExist(err) {
+ t.Fatalf("old incarnation remains: %v", err)
+ }
+ f.base.Host, f.target.Host = "other.example", "other.example"
+ if err := os.MkdirAll(filepath.Dir(provision.NestedDBPath(f.data, f.base.Host)), 0700); err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "other host")
+ other, receipts := f.evidence(12, true, "missing")
+ if _, err := f.park(other, receipts); err != nil {
+ t.Fatal(err)
+ }
+ f.complete(other)
+ f.retained(next, "next incarnation", r.ReapAfter)
+}
+
+func TestServingParkLedgerDiscardPendingBlocksNextRetirement(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ old, stops := f.evidence(10, true, "missing")
+ r, err := f.park(old, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(old)
+ f.put("", "unlaunched")
+ pending, receipts := f.evidence(11, false, "prelaunch")
+ if _, err := f.park(pending, receipts); err != nil {
+ t.Fatal(err)
+ }
+ f.put("", "next tenant writes")
+ next, receipts := f.evidence(12, false, "started")
+ if _, err := f.park(next, receipts); err == nil {
+ t.Fatal("unfinished discard overtaken")
+ }
+ f.retained(old, "legacy", r.ReapAfter)
+}
+
+func TestServingParkLedgerRefusesPrematureTombstone(t *testing.T) {
+ f := newServingParkDiskFixture(t)
+ f.put("", "legacy")
+ a, stops := f.evidence(10, true, "missing")
+ r, err := f.park(a, stops)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.complete(a)
+ r.Complete, r.Deleting, r.Closed, r.Cause = true, true, true, "expiry"
+ if err := f.ledger.write("retirement-"+a.ID+".json", r); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.ledger.read(a.ID); err == nil {
+ t.Fatal("incomplete deletion accepted as a tombstone")
+ }
+ if b, e := os.ReadFile(f.path(a, "")); e != nil || string(b) != "legacy" {
+ t.Fatalf("malformed record changed data: %q %v", b, e)
+ }
+}
diff --git a/internal/host/claimservingpark_other.go b/internal/host/claimservingpark_other.go
new file mode 100644
index 000000000..e6ed6f549
--- /dev/null
+++ b/internal/host/claimservingpark_other.go
@@ -0,0 +1,20 @@
+//go:build !linux
+
+package host
+
+import (
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type servingParkFS struct{}
+
+func newServingParkFS(string, rootstate.Store) *servingParkFS { return &servingParkFS{} }
+func (*servingParkFS) Capture(servingParkAuthorization) (servingParkSource, error) {
+ return servingParkSource{}, hostbox.ErrUnsupported
+}
+func (*servingParkFS) Prepare(*servingParkRecord, func() error) error { return hostbox.ErrUnsupported }
+func (*servingParkFS) Move(*servingParkRecord, func() error) error { return hostbox.ErrUnsupported }
+func (*servingParkFS) Delete(*servingParkRecord, func() error) error { return hostbox.ErrUnsupported }
+
+func (*servingParkFS) CheckParked(*servingParkRecord) error { return hostbox.ErrUnsupported }
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go
new file mode 100644
index 000000000..dd09c056a
--- /dev/null
+++ b/internal/host/claimservingpark_records.go
@@ -0,0 +1,491 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+// servingParkLedger has no production caller until root authorization and the
+// journal transport are bound. Its callbacks are private root implementation
+// seams; no request can supply a source, disposition, timestamp or completion.
+type servingParkLedger struct {
+ state rootstate.Store
+ fs servingParkFilesystem
+ journal *hostbox.LaunchJournal
+ now func() time.Time
+}
+
+type servingParkFile struct {
+ Name string
+ Present bool
+ Device, Inode, Mount, Bytes uint64
+ Mode, Links uint32
+ Moved, DeleteIntent, Deleted bool
+}
+
+type servingParkSource struct {
+ Directory servingArtifactDirectory
+ Parent servingArtifactDirectory
+ Files [4]servingParkFile
+}
+
+type servingParkRecord struct {
+ Version int
+ Authorization servingParkAuthorization
+ Source servingParkSource
+ Receipts []hostguard.FenceReceipt
+ Disposition servingParkDisposition
+ Event servingArtifactDirectory
+ Prepared, Parked, Complete, Deleting, Closed bool
+ ParkedAt, ReapAfter time.Time
+ Cause string
+}
+
+type servingLegacyConsumption struct {
+ Authorization servingParkAuthorization
+ Source servingParkSource
+}
+
+type servingParkSlot struct {
+ Version int
+ Box, Host, ID, Pending string
+}
+
+type servingParkFilesystem interface {
+ // Capture validates a root-authenticated source before any slot is changed.
+ Capture(servingParkAuthorization) (servingParkSource, error)
+ Prepare(*servingParkRecord, func() error) error
+ Move(*servingParkRecord, func() error) error
+ Delete(*servingParkRecord, func() error) error
+ CheckParked(*servingParkRecord) error
+}
+
+func servingParkKey(box, host string) string {
+ sum := sha256.Sum256([]byte(box + "\x00" + host))
+ return hex.EncodeToString(sum[:])
+}
+
+func servingParkDecode(body []byte, out any) error {
+ d := json.NewDecoder(bytes.NewReader(body))
+ d.DisallowUnknownFields()
+ if d.Decode(out) != nil || d.Decode(new(any)) != io.EOF {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+func (l *servingParkLedger) write(name string, value any) error {
+ body, err := json.Marshal(value)
+ if err != nil {
+ return err
+ }
+ return l.state.Write(name, body)
+}
+
+func (l *servingParkLedger) read(id string) (servingParkRecord, error) {
+ var r servingParkRecord
+ if len(id) != 64 {
+ return r, hostbox.ErrChanged
+ }
+ if _, err := hex.DecodeString(id); err != nil {
+ return r, hostbox.ErrChanged
+ }
+ body, err := l.state.Read("retirement-" + id + ".json")
+ if err != nil {
+ return r, err
+ }
+ if err = servingParkDecode(body, &r); err != nil {
+ return r, err
+ }
+ if err = validateServingParkRecord(r); err != nil || r.Authorization.ID != id {
+ return r, hostbox.ErrChanged
+ }
+ return r, nil
+}
+
+func validateServingParkRecord(r servingParkRecord) error {
+ 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 {
+ return hostbox.ErrChanged
+ }
+ if a.Legacy {
+ if a.Predecessor != (servingArtifactIdentity{}) || a.TagsFrom != 0 || r.Source.Files[1].Present {
+ return hostbox.ErrChanged
+ }
+ } else {
+ p := a.Predecessor
+ if p.Host != s.Host || p.Box != s.Box || p.Region != s.Region || p.Incarnation != s.Incarnation || p.Location != s.Location || p.Generation == "" || p.Generation == s.Generation || p.Stamp == 0 || p.Stamp != p.Session || p.Stamp >= s.Stamp || a.TagsFrom != p.Stamp {
+ return hostbox.ErrChanged
+ }
+ }
+ if len(r.Receipts) == 0 {
+ return hostbox.ErrChanged
+ }
+ seen := make(map[string]bool)
+ for _, receipt := range r.Receipts {
+ t := receipt.Target
+ if t.Host != s.Host || t.Box != s.Box || t.Region != s.Region || t.Incarnation != s.Incarnation || t.Stamp == 0 || t.Stamp >= s.Stamp || t.Session >= s.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.FenceID == "" || seen[t.FenceID] || verifyRetirementReceipt(context.Background(), receipt) != nil {
+ return hostbox.ErrChanged
+ }
+ if !a.Legacy && (t.Stamp != a.Predecessor.Stamp || t.Session != a.Predecessor.Session) {
+ return hostbox.ErrChanged
+ }
+ seen[t.FenceID] = true
+ }
+ for i, suffix := range []string{"", ".gen", "-wal", "-shm"} {
+ f := r.Source.Files[i]
+ if f.Name != s.Host+".db"+suffix {
+ return hostbox.ErrChanged
+ }
+ if f.Present {
+ if f.Device != r.Source.Directory.Device || f.Mount != r.Source.Directory.Mount || f.Inode == 0 || f.Mode != 0100000 || f.Links != 1 {
+ return hostbox.ErrChanged
+ }
+ } else if f.Device != 0 || f.Inode != 0 || f.Mount != 0 || f.Mode != 0 || f.Links != 0 || f.Bytes != 0 || f.Moved || f.DeleteIntent || f.Deleted {
+ return hostbox.ErrChanged
+ }
+ if f.Deleted && (!f.DeleteIntent || !r.Deleting) || f.DeleteIntent && !f.Moved {
+ return hostbox.ErrChanged
+ }
+ }
+ if r.Disposition != servingParkRetain && r.Disposition != servingParkDiscard && r.Disposition != servingParkArtifactFree {
+ return hostbox.ErrChanged
+ }
+ if (r.Disposition == servingParkArtifactFree) == r.Source.Files[0].Present {
+ return hostbox.ErrChanged
+ }
+ if r.Prepared && (r.Event.Inode == 0 || r.Event.Device != r.Source.Directory.Device || r.Event.Mount != r.Source.Directory.Mount) {
+ return hostbox.ErrChanged
+ }
+ if r.Parked {
+ if !r.Prepared || r.ParkedAt.IsZero() || !r.ReapAfter.Equal(r.ParkedAt.Add(7*24*time.Hour)) {
+ return hostbox.ErrChanged
+ }
+ for _, f := range r.Source.Files {
+ if f.Present && !f.Moved {
+ return hostbox.ErrChanged
+ }
+ }
+ } else if !r.ParkedAt.IsZero() || !r.ReapAfter.IsZero() || r.Complete {
+ return hostbox.ErrChanged
+ }
+ if r.Deleting && (r.Cause != "discard" && r.Cause != "replacement" && r.Cause != "expiry" && r.Cause != "pressure") {
+ return hostbox.ErrChanged
+ }
+ if r.Deleting && !r.Parked {
+ return hostbox.ErrChanged
+ }
+ if r.Closed && !r.Deleting {
+ return hostbox.ErrChanged
+ }
+ if r.Closed {
+ for _, item := range r.Source.Files {
+ if item.Present && !item.Deleted {
+ return hostbox.ErrChanged
+ }
+ }
+ }
+ if r.Complete && r.Disposition != servingParkRetain && !r.Closed {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
+
+func (l *servingParkLedger) save(r servingParkRecord) error {
+ if err := validateServingParkRecord(r); err != nil {
+ return err
+ }
+ return l.write("retirement-"+r.Authorization.ID+".json", r)
+}
+
+func (l *servingParkLedger) slot(a servingParkAuthorization) (servingParkSlot, error) {
+ s := servingParkSlot{Version: 1, Box: a.Successor.Box, Host: a.Successor.Host}
+ b, err := l.state.Read("slot-" + servingParkKey(s.Box, s.Host) + ".json")
+ if errors.Is(err, os.ErrNotExist) {
+ return s, nil
+ }
+ if err != nil {
+ return s, err
+ }
+ var saved servingParkSlot
+ if servingParkDecode(b, &saved) != nil || saved.Version != 1 || saved.Box != s.Box || saved.Host != s.Host {
+ return s, hostbox.ErrChanged
+ }
+ if saved.ID != "" {
+ r, err := l.read(saved.ID)
+ if err != nil || r.Disposition != servingParkRetain || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
+ return s, hostbox.ErrChanged
+ }
+ }
+ if saved.Pending != "" {
+ r, err := l.read(saved.Pending)
+ if err != nil || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
+ return s, hostbox.ErrChanged
+ }
+ }
+ return saved, nil
+}
+
+// withNamespace precedes the root host guard, slot lock and record lock. The
+// authority callback must acquire the host guard and independently authenticate
+// the claim, receipts, state directory and launch history while it is held.
+func (l *servingParkLedger) withNamespace(ctx context.Context, fn func() error) error {
+ if ctx == nil || l == nil || l.state == nil || l.fs == nil || l.now == nil || fn == nil {
+ return hostbox.ErrUnsupported
+ }
+ unlock, err := l.state.Lock(ctx, "namespace.lock")
+ if err != nil {
+ return err
+ }
+ defer unlock()
+ if err = ctx.Err(); err != nil {
+ return err
+ }
+ return fn()
+}
+
+// parkLocked runs inside independently verified root host authority. Fresh
+// capture is never substituted for the immutable source on a replay.
+func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) {
+ var zero servingParkRecord
+ if err := ctx.Err(); err != nil {
+ return zero, err
+ }
+ unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock")
+ if err != nil {
+ return zero, err
+ }
+ defer unlock()
+ slot, err := l.slot(a)
+ if err != nil {
+ return zero, err
+ }
+ if slot.Pending != "" && slot.Pending != a.ID {
+ return zero, hostbox.ErrChanged
+ }
+ r, err := l.read(a.ID)
+ if errors.Is(err, os.ErrNotExist) {
+ source, e := l.fs.Capture(a)
+ if e != nil {
+ return zero, e
+ }
+ disposition := servingParkArtifactFree
+ if a.Legacy {
+ if source.Files[1].Present {
+ return zero, hostbox.ErrChanged
+ }
+ if source.Files[0].Present {
+ disposition = servingParkRetain
+ }
+ } else {
+ disposition, e = servingGenerationParkDisposition(ctx, l.journal, a.Predecessor, receipts, source.Files[0].Present)
+ if e != nil {
+ return zero, e
+ }
+ }
+ r = servingParkRecord{Version: 1, Authorization: a, Source: source, Receipts: receipts, Disposition: disposition}
+ if e = validateServingParkRecord(r); e != nil {
+ return zero, e
+ }
+ // Consumption survives slot replacement, expiry and park tombstones.
+ if a.Legacy {
+ key := "legacy-" + servingParkKey(a.Successor.Box, a.Successor.Host+"\x00"+a.Successor.Incarnation) + ".json"
+ var consumed servingLegacyConsumption
+ want := servingLegacyConsumption{Authorization: a, Source: source}
+ body, e := l.state.Read(key)
+ if e == nil {
+ if servingParkDecode(body, &consumed) != nil || consumed != want {
+ return zero, hostbox.ErrChanged
+ }
+ } else if !errors.Is(e, os.ErrNotExist) {
+ return zero, e
+ }
+ if e = l.write(key, want); e != nil {
+ return zero, e
+ }
+ }
+ if e = l.save(r); e != nil {
+ return zero, e
+ }
+ } else if err != nil {
+ return zero, err
+ }
+ if r.Authorization != a {
+ return zero, hostbox.ErrChanged
+ }
+ if r.Closed || r.Complete {
+ return r, nil
+ }
+ if slot.Pending == "" {
+ slot.Pending = a.ID
+ if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
+ return zero, err
+ }
+ }
+ if r.Deleting {
+ if r.Disposition == servingParkRetain || r.Cause != "discard" {
+ return zero, hostbox.ErrChanged
+ }
+ if err = l.deleteLocked(ctx, &r, "discard"); err != nil {
+ return zero, err
+ }
+ return r, nil
+ }
+ if r.Disposition == servingParkRetain && slot.ID != a.ID {
+ // A completed replay can never reacquire a slot it has lost.
+ if r.Parked || r.Complete {
+ return zero, hostbox.ErrChanged
+ }
+ fresh, e := l.fs.Capture(a)
+ if e != nil || fresh != r.Source {
+ return zero, hostbox.ErrChanged
+ }
+ if slot.ID != "" {
+ old, e := l.read(slot.ID)
+ if e != nil || !old.Complete {
+ return zero, hostbox.ErrChanged
+ }
+ if !old.Closed {
+ cause := "replacement"
+ if old.Deleting {
+ cause = old.Cause
+ }
+ if e = l.deleteLocked(ctx, &old, cause); e != nil {
+ return zero, e
+ }
+ }
+ }
+ slot.ID = a.ID
+ if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
+ return zero, err
+ }
+ }
+ save := func() error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ return l.save(r)
+ }
+ if err = l.fs.Prepare(&r, save); err != nil {
+ return zero, err
+ }
+ if err = l.fs.Move(&r, save); err != nil {
+ return zero, err
+ }
+ if !r.Parked {
+ r.Parked, r.ParkedAt = true, l.now().UTC()
+ r.ReapAfter = r.ParkedAt.Add(7 * 24 * time.Hour)
+ if err = save(); err != nil {
+ return zero, err
+ }
+ }
+ // Even discarded restores are first isolated by recorded rename into the
+ // root-owned event. A raced source replacement is never unlinked in place.
+ if r.Disposition != servingParkRetain && !r.Closed {
+ if err = l.deleteLocked(ctx, &r, "discard"); err != nil {
+ return zero, err
+ }
+ }
+ return r, nil
+}
+
+// completeLocked follows a fresh root check of the successor's paired tags.
+// Its caller holds namespace and host authority, never an agent Work lock.
+func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a servingArtifactIdentity) error {
+ r, err := l.read(id)
+ if err != nil {
+ return err
+ }
+ if r.Authorization.Successor != a || !r.Parked {
+ return hostbox.ErrChanged
+ }
+ slot, err := l.slot(r.Authorization)
+ if err != nil {
+ return err
+ }
+ if !r.Complete {
+ if slot.Pending != id {
+ return hostbox.ErrChanged
+ }
+ if err = l.fs.CheckParked(&r); err != nil {
+ return err
+ }
+ if r.Disposition == servingParkRetain {
+ if slot.ID != id || r.Closed || r.Deleting {
+ return hostbox.ErrChanged
+ }
+ } else if !r.Closed {
+ return hostbox.ErrChanged
+ }
+ if err = ctx.Err(); err != nil {
+ return err
+ }
+ r.Complete = true
+ if err = l.save(r); err != nil {
+ return err
+ }
+ }
+ if slot.Pending == id {
+ slot.Pending = ""
+ return l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot)
+ }
+ return nil
+}
+
+func (l *servingParkLedger) deleteLocked(ctx context.Context, r *servingParkRecord, cause string) error {
+ if r.Closed {
+ return nil
+ }
+ if cause != "discard" && (!r.Complete || r.Disposition != servingParkRetain) {
+ return hostbox.ErrChanged
+ }
+ if cause == "discard" && r.Disposition == servingParkRetain {
+ return hostbox.ErrChanged
+ }
+ if r.Deleting && r.Cause != cause {
+ return hostbox.ErrChanged
+ }
+ save := func() error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ return l.save(*r)
+ }
+ r.Deleting, r.Cause = true, cause
+ if err := save(); err != nil {
+ return err
+ }
+ if err := l.fs.Delete(r, save); err != nil {
+ return err
+ }
+ r.Closed = true
+ return save()
+}
+
+// reapLocked needs only namespace/slot/record authority; it never takes a host
+// guard. Pressure selection and authenticated replacement are separate callers.
+func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error {
+ r, err := l.read(id)
+ if err != nil {
+ return err
+ }
+ if r.Closed {
+ return nil
+ }
+ if !r.Complete || r.Disposition != servingParkRetain || l.now().Before(r.ReapAfter) {
+ return hostbox.ErrChanged
+ }
+ return l.deleteLocked(ctx, &r, "expiry")
+}
diff --git a/internal/host/claimservingparksource_linux.go b/internal/host/claimservingparksource_linux.go
new file mode 100644
index 000000000..3d8f89555
--- /dev/null
+++ b/internal/host/claimservingparksource_linux.go
@@ -0,0 +1,73 @@
+//go:build linux
+
+package host
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancefs"
+ "github.com/carlosframework/platform/internal/provision"
+ "golang.org/x/sys/unix"
+)
+
+// servingParkStateDirectory accepts only root installation and root inventory
+// snapshots. The future root handler must read both locally under host authority;
+// this function is not a transport decoder and accepts no registry row.
+func servingParkStateDirectory(data string, o hostclaim.Observed, installation hostguard.RootInstallation, in hostbox.StopInventory, revision uint64, actor hostbox.Actor) (servingArtifactDirectory, error) {
+ var zero servingArtifactDirectory
+ current, err := servingWriterIdentity(o, actor.Box, actor.Region)
+ if err != nil {
+ return zero, err
+ }
+ if !filepath.IsAbs(data) || filepath.Clean(data) != data || strings.ContainsAny(current.Host, "/\\\x00") || current.Host == "." || current.Host == ".." {
+ return zero, hostbox.ErrChanged
+ }
+ h := o.Claim.Holder
+ if h.Family != "instance" || installation.Migration == 0 || installation.Empty || installation.Host != current.Host || installation.Incarnation != current.Incarnation || installation.Stamp == 0 || installation.Stamp > current.Stamp || installation.Account != h.Account || installation.App != h.App || installation.Record != h.Key || installation.Box != actor.Box || installation.Enrollment != actor.Enrollment || actor.Enrollment == "" || actor.Revision == 0 || revision == 0 || installation.InventoryRevision != revision {
+ return zero, hostbox.ErrChanged
+ }
+ if installation.Stamp != current.Stamp {
+ if _, err := servingPredecessor(o.Claim, current, installation.Stamp, ""); err != nil {
+ return zero, err
+ }
+ }
+ if !in.Complete || in.Host != installation.Host || in.Incarnation != installation.Incarnation || in.InstallationStamp != installation.Stamp || in.Box != actor.Box || in.Enrollment != actor.Enrollment || in.DirectoryRevision != fmt.Sprint(actor.Revision) {
+ return zero, hostbox.ErrTypedInventory
+ }
+ relative := filepath.ToSlash(filepath.Join("instances", provision.InstanceID(current.Host)))
+ sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", in.Host, in.Incarnation, in.InstallationStamp, in.Box)))
+ owner := hex.EncodeToString(sum[:])
+ var selected hostbox.StopInventoryEntry
+ for _, entry := range in.Entries {
+ if entry.Kind != "path" || entry.ManagedRoot != "data" || entry.RelativePath != relative {
+ continue
+ }
+ if selected.ArtifactID != "" || entry.ArtifactID == "" || entry.Ownership != owner || entry.Materialization != "materialized" || entry.Type != unix.S_IFDIR || entry.Device == 0 || entry.Inode == 0 {
+ return zero, hostbox.ErrTypedInventory
+ }
+ selected = entry
+ }
+ if selected.ArtifactID == "" {
+ return zero, hostbox.ErrTypedInventory
+ }
+ dir, err := instancefs.OpenDirBeneath(data, relative)
+ if err != nil {
+ return zero, err
+ }
+ defer dir.Close()
+ id, err := servingParkDirID(dir)
+ if err != nil {
+ return zero, err
+ }
+ if id.Device != selected.Device || id.Inode != selected.Inode {
+ return zero, hostbox.ErrChanged
+ }
+ return id, nil
+}
diff --git a/internal/host/claimservingparksource_linux_test.go b/internal/host/claimservingparksource_linux_test.go
new file mode 100644
index 000000000..2929b82ed
--- /dev/null
+++ b/internal/host/claimservingparksource_linux_test.go
@@ -0,0 +1,83 @@
+//go:build linux
+
+package host
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "golang.org/x/sys/unix"
+)
+
+func TestServingParkSourceUsesRootInventory(t *testing.T) {
+ for _, attack := range []string{"none", "unmaterialized", "duplicate", "owner", "revision", "enrollment", "record", "stamp", "foreign", "missing", "flat", "symlink", "replacement"} {
+ t.Run(attack, func(t *testing.T) {
+ f, _, _, inputs := servingRetirementFixture(t)
+ o := servingAdmit(t, f, &inputs, "G")
+ h := o.Claim.Holder
+ actor := hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7}
+ root := hostguard.RootInstallation{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Migration: 1, InventoryRevision: 3, Account: h.Account, App: h.App, Record: h.Key, Box: actor.Box, Enrollment: actor.Enrollment}
+ d, err := os.Open(filepath.Dir(inputs.DBPath))
+ if err != nil {
+ t.Fatal(err)
+ }
+ id, err := servingParkDirID(d)
+ d.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ owner := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, actor.Box)))
+ in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "7", Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + provision.InstanceID(o.Host), ArtifactID: "state", Ownership: hex.EncodeToString(owner[:]), Materialization: "materialized", Device: id.Device, Inode: id.Inode, Type: unix.S_IFDIR}}}
+ switch attack {
+ case "unmaterialized":
+ in.Entries[0].Materialization = "intended"
+ case "duplicate":
+ in.Entries = append(in.Entries, in.Entries[0])
+ case "owner":
+ in.Entries[0].Ownership = "forged"
+ case "revision":
+ root.InventoryRevision++
+ case "enrollment":
+ actor.Enrollment = "foreign"
+ case "record":
+ root.Record = "foreign"
+ case "stamp":
+ root.Stamp++
+ case "foreign":
+ in.Host = "other.example"
+ case "missing":
+ in.Entries = nil
+ case "flat":
+ in.Entries[0].RelativePath = "state"
+ case "symlink", "replacement":
+ dir := filepath.Dir(inputs.DBPath)
+ if err := os.Rename(dir, dir+".original"); err != nil {
+ t.Fatal(err)
+ }
+ if attack == "symlink" {
+ err = os.Symlink(dir+".original", dir)
+ } else {
+ err = os.Mkdir(dir, 0700)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ got, err := servingParkStateDirectory(f.guard.DataDir, o, root, in, 3, actor)
+ if attack == "none" {
+ if err != nil || got != id {
+ t.Fatalf("source: %+v %v", got, err)
+ }
+ } else if err == nil {
+ t.Fatal("unproved source accepted")
+ }
+ })
+ }
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 232c82773..6ce8bb5b3 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -472,6 +472,9 @@ func Reviewed(ctx context.Context, s Store, k string) error {
// BEGIN REVIEWED INVENTORY
var plan9B8CensusInventory = []censusEntry{
+ // rev3=PROPOSED-seed: local instances mount/state references; no deployment record writer. Root authority remains in the seed overlay.
+ {"internal/host/claimservingpark_linux.go", "servingParkFS.parent", "key-site", "lit:instances", "1d241a7d446bbbade6e460efd911863f8a5246b8ac1d47b2548034e3feb2f216"},
+ {"internal/host/claimservingparksource_linux.go", "servingParkStateDirectory", "key-site", "lit:instances", "ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "10d09b5ab6d07d316437233c9a65bf0bcdfe1083db4d0769fa8303b255dfd0ac"},
{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "10a99ff4b355907831a904010b7019d480ce93f2f595e99e076d3c702d10ca2b"},
{"internal/host/rootrecovery_rebind.go", "type recoveryRebindRequest", "key-site", "key:RecordKey", "b5db3e4dcd4807edea1d053eb1787fbf222be97d3f892e2ab66771457a51dc3d"},
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 4de2c4271..e5864bd32 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,5 +1,5 @@
-# Files seed checkpoint 2 overlay; pending Paul’s census ruling.
-# No root parking or deletion authority is implemented in this checkpoint.
+# Files seed checkpoint 3 overlay; pending Paul’s census ruling.
+# Root filesystem and retirement-ledger mechanisms have test callers only; production binding is refused.
# Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed.
# Every classification below is rev3=PROPOSED-seed, not an approved disposition.
internal/host/claimconstruction.go OpenProductionClaimAssembly authority C,G rev3=PROPOSED-seed; box process identity and provider bindings; serving dispatch independent of moves; serving enrollment required before a runtime can be built file:Read@4150,file:Read@9550,file:ReadFile@3694,registry:Host@9577,whole:Route@9665 088f34c7fe1b4f9bb7a3187d40d60bff0fd811ebf1e1888ce3d4781dc4de80d7
@@ -31,6 +31,41 @@ internal/host/claimservingretire.go servingPredecessor authority C,J4 rev3=PROPO
internal/host/claimservingretire.go servingRetirementID authority C,J4 rev3=PROPOSED-seed; immutable predecessor and original successor tuples; deterministic journal identity independent of claim operation IDs c984974d17e621d7fe9c276a03ae789b86789abbccbb82a09d06f55c3bf13819
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 ae2fa116ece3bbd596baa02971ef69fba597ce2ab4135abb6bf9caae6d326202
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
-@coverage internal/host 0e48977bb98fe6f8c707566b661fd034cda83afeb408512820466e0d31f972e7
+internal/host/claimservingpark_linux.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; root-only filesystem dependencies, fixed nosymfollow validation and private state store; no production caller 9a72f9bb7c62e85199e303411b42e01b0b016a2a5cf2d9c84e2588b5f36b52e2
+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 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.CheckParked authority C,J4 rev3=PROPOSED-seed; all recorded event identities, unexpected children and source absence before root completion file:Readdirnames@822 144c738d31a4b6495d742310dc3ac171bd0d70bff902395a8cc77119191ccd22
+internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; root-recorded event and artifact identities; validates the whole set before journaled per-name unlink, fsync and progress; no recursive deletion file:Readdirnames@341 8837ae9e8947479346f6dddeb8ea8f13290ad56d9d2b96f315e203f5c2e1646a
+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 before persisting event identity; existing unrecorded namespace is refused file:Prepare@24,file:Readdirnames@890 2480401a7da95a24a7557be0b8adff67394689abb418e36d0fafecdb8c1d827b
+internal/host/claimservingpark_linux.go servingParkFS.event authority C,J4 rev3=PROPOSED-seed; recorded namespace parent and private event mount/device/inode identities 8e2ce5bf2f5931e702b847d664bd5cff5fbf90c71ed74b5a2d52ca06d9e0da7c
+internal/host/claimservingpark_linux.go servingParkFS.namespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace record, effective parent mount and root-owned park directory; unknown roots refused without adoption file:Read@211 788a7186623346d4a623c2924cf33f2797b40515b6c42db8154bd38168cc8cd4
+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_other.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 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.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; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 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_records.go servingParkDecode authority C,J4 rev3=PROPOSED-seed; strict private-record JSON with unknown-field and trailing-value refusal 189c7944ea9f5d269d39ed6a869b6bd739e3033195f06cc1f2c9f23a23180184
+internal/host/claimservingpark_records.go servingParkKey authority C,J4 rev3=PROPOSED-seed; box/host identity used for stable per-host namespace and slot keys 67518302550380dbf9b741a73758e5c94307eff70ff8a09cc56763a8aa794d86
+internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 246eb0689f2f36a968c60d9241f52d2f3cf7ebf76b025092e1658f1b7b495236
+internal/host/claimservingpark_records.go servingParkLedger.deleteLocked authority C,J4 rev3=PROPOSED-seed; complete retained record or proved discard disposition and persisted cause/progress before filesystem deletion 1545950c564d51d9c29b0d76daca1ddec5a159481698b11accd4dd7411157b37
+internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3312,file:Read@1564 d248f233de019cf9e28cf89bbf1e41f8e9c23089556ca709d18a9b076a04c09c
+internal/host/claimservingpark_records.go servingParkLedger.read authority C,J4 rev3=PROPOSED-seed; private retirement record by validated deterministic ID and strict record invariants file:Read@249 868c8c3f5682dc36738d6960d57f2f1fcc6512374c8f8e0790187bf69cf3de64
+internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry 2352daae92630f8e33bd7c9bdd70f382de9da9be559de8522982eb73cf935c09
+internal/host/claimservingpark_records.go servingParkLedger.save authority C,J4 rev3=PROPOSED-seed; validated private retirement record before durable rootstate write b6f940b1e21cf1beb67638effea958d2b1119695494a562e22bbbe0469d8af49
+internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes file:Read@179 5c0d897b1bc9f0733d1dad18a90ce27900e371aaae2940e49cded65888d1c084
+internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; production host-guard composition remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac
+internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571
+internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7
+internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants 0ab78151f2243224751d32fb309db9e36eed27487935c97f9531a72adf42fc62
+internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; snapshot acquisition remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35
+@coverage internal/host cc1a23e90ccf77b923b6c02649b3823cfbb0454368c06571e8e5292315becfc6
@coverage internal/hostbox 72c9b640d98c779a044579f70a103a68cd9819c794a0bb3e53f378d39ad11419
@coverage internal/hostguard 51408b42f01be94a232e395494e5d154cc57736e32a6519ac6c03e9ff3d091e6