carlos / platform Public

WIP files seed checkpoint 20 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6a557397bd6cfd63debb9b27603bcb13880295f8 parent 757266c
7 files changed, +575 −18
  • internal/host/claimservingboot_linux.go +9 −5
  • internal/host/claimservingpark_linux.go +9 −2
  • internal/host/claimservingpark_rootrunner_linux_test.go +27 −0
  • internal/host/claimservingquarantine_linux.go +7 −2
  • internal/host/claimservingtopology_linux.go +199 −0
  • internal/host/claimservingtopology_linux_test.go +307 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +17 −9

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	350.739s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.025s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.325s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.703s
ok  	github.com/carlosframework/platform/internal/activator	234.615s
ok  	github.com/carlosframework/platform/internal/agentsock	0.022s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.027s
ok  	github.com/carlosframework/platform/internal/appconfig	2.370s
ok  	github.com/carlosframework/platform/internal/apperr	0.094s
ok  	github.com/carlosframework/platform/internal/applog	0.147s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.744s
?   	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.009s
ok  	github.com/carlosframework/platform/internal/caps	0.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.694s
ok  	github.com/carlosframework/platform/internal/changelog	0.322s
ok  	github.com/carlosframework/platform/internal/clamd	0.056s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	368.631s
?   	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.852s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.012s
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.006s
ok  	github.com/carlosframework/platform/internal/docsite	0.008s
ok  	github.com/carlosframework/platform/internal/domains	2.181s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.008s
ok  	github.com/carlosframework/platform/internal/edge	26.157s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.126s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.021s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.031s
ok  	github.com/carlosframework/platform/internal/host	154.814s
ok  	github.com/carlosframework/platform/internal/hostactors	0.438s
ok  	github.com/carlosframework/platform/internal/hostbox	2.296s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.519s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.854s
ok  	github.com/carlosframework/platform/internal/hostfence	0.034s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.561s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	0.970s
ok  	github.com/carlosframework/platform/internal/hostops	1.434s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.458s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.154s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	1.814s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.713s
ok  	github.com/carlosframework/platform/internal/instancefs	0.023s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.022s
ok  	github.com/carlosframework/platform/internal/layout	1.444s
ok  	github.com/carlosframework/platform/internal/lease	9.484s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.023s
ok  	github.com/carlosframework/platform/internal/ledger	0.034s
ok  	github.com/carlosframework/platform/internal/mail	0.156s
ok  	github.com/carlosframework/platform/internal/maintenance	0.090s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.505s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.073s
ok  	github.com/carlosframework/platform/internal/metrics	0.010s
ok  	github.com/carlosframework/platform/internal/moves	21.284s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.748s
ok  	github.com/carlosframework/platform/internal/objstore	0.189s
ok  	github.com/carlosframework/platform/internal/ops	0.167s
ok  	github.com/carlosframework/platform/internal/park	0.204s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	11.910s
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.150s
ok  	github.com/carlosframework/platform/internal/relay	7.237s
ok  	github.com/carlosframework/platform/internal/release	20.247s
ok  	github.com/carlosframework/platform/internal/rollout	3.653s
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.010s
ok  	github.com/carlosframework/platform/internal/rootstate	0.024s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.006s
ok  	github.com/carlosframework/platform/internal/schedule	2.282s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.570s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.633s
ok  	github.com/carlosframework/platform/internal/status	0.042s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.270s
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.003s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.556s
=== 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.023s
=== RUN   TestT4StorageProbeMinio
2026/10/14 21:14:13 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE81D4ECF8D013, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/internal/host/claimservingboot_linux.go b/internal/host/claimservingboot_linux.go
index 9ad30c1b6..342c8154b 100644
--- a/internal/host/claimservingboot_linux.go
+++ b/internal/host/claimservingboot_linux.go
@@ -32,15 +32,19 @@ func (f *servingParkFS) currentBoot() (string, error) {
if err != nil {
return "", err
}
- if len(id) != 36 || id[8] != '-' || id[13] != '-' || id[18] != '-' || id[23] != '-' {
+ if !validServingParkBootID(id) {
return "", hostbox.ErrChanged
}
+ 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)
- if err != nil || len(b) != 16 || hex.EncodeToString(b) != raw || raw == strings.Repeat("0", 32) {
- return "", hostbox.ErrChanged
- }
- return id, nil
+ 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
diff --git a/internal/host/claimservingpark_linux.go b/internal/host/claimservingpark_linux.go
index 366166b5e..6b8cbd9ff 100644
--- a/internal/host/claimservingpark_linux.go
+++ b/internal/host/claimservingpark_linux.go
@@ -17,7 +17,8 @@ import (
)
type servingParkNamespace struct {
- BootID string `json:",omitempty"`
+ Topology *servingParkTopology `json:",omitempty"`
+ BootID string `json:",omitempty"`
Version int
Parent, Root servingArtifactDirectory
}
@@ -28,6 +29,7 @@ type servingParkFS struct {
owner uint32
mount func(string) (bool, error)
boot func() (string, error)
+ handle func(*os.File) (servingParkPersistentDirectory, error)
rename func(int, string, int, string, uint) error
sync func(int) error
unlink func(int, string, int) error
@@ -36,7 +38,7 @@ type servingParkFS struct {
// 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, boot: servingParkBootID, rename: unix.Renameat2, sync: unix.Fsync, unlink: unix.Unlinkat}
+ return &servingParkFS{data: data, state: state, owner: 0, mount: instancefs.MountNoSymfollow, boot: servingParkBootID, handle: servingParkPersistentDir, rename: unix.Renameat2, sync: unix.Fsync, unlink: unix.Unlinkat}
}
func servingParkDirID(f *os.File) (servingArtifactDirectory, error) {
@@ -154,6 +156,11 @@ func (f *servingParkFS) namespace() (*os.File, servingParkNamespace, error) {
return nil, n, hostbox.ErrChanged
}
n = servingParkNamespace{Version: 1, Parent: id, Root: rid, BootID: boot}
+ n.Topology, e = f.captureTopology(parent, d, n)
+ if e != nil {
+ d.Close()
+ return nil, n, e
+ }
if e = f.sync(int(parent.Fd())); e == nil {
l := servingParkLedger{state: f.state}
e = l.write("namespace.json", n)
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go
index 58391eb6a..d5df3d135 100644
--- a/internal/host/claimservingpark_rootrunner_linux_test.go
+++ b/internal/host/claimservingpark_rootrunner_linux_test.go
@@ -116,6 +116,11 @@ func servingRunnerPark(t *testing.T, data string, state rootstate.Store) serving
fs := newServingParkFS(data, state)
a := servingParkAuthorization{Successor: current, OriginalSuccessor: current, Directory: id, Legacy: true}
a.ID = servingRetirementID(a.Predecessor, current, true)
+ // The helper's installation authorization remains a fixture. Capture the
+ // parent through the production recorder so reboot can inspect its proof.
+ if err := fs.recordNamespaceParent(t.Context(), current, id); err != nil {
+ t.Fatal(err)
+ }
source, err := fs.Capture(a)
if err != nil {
t.Fatal(err)
@@ -167,6 +172,13 @@ func TestRootServingParkUnitPayload(t *testing.T) {
defer state.Close()
r := servingRunnerPark(t, data, state)
fs := newServingParkFS(data, state)
+ // Exercise persistent handles inside the unchanged hardened unit. A
+ // simulated boot lets the read-only checker run; it enables no effects.
+ fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ if _, err := fs.inspectRebootTopology(t.Context()); err != nil {
+ t.Fatal("installed-unit topology inspection", err)
+ }
+ fs.boot = servingParkBootID
r.Deleting, r.Cause = true, "expiry"
save := func() error { return (&servingParkLedger{state: state}).write("fixture.json", r) }
fs.unlink = func(fd int, name string, flags int) error {
@@ -391,6 +403,21 @@ func TestRootRebootServingParkChangedMountPhase2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ // Prove independent recognition after a real reboot before exercising the
+ // old-authority refusals below. No namespace or retirement is rewritten.
+ parentBefore, err := state.Read("namespace-parent.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ observation, err := fs.inspectRebootTopology(t.Context())
+ if err != nil || observation.BootID != strings.TrimSpace(string(boot)) || observation.Previous.BootID != saved.Boot || observation.CurrentParent.Mount == observation.Previous.Parent.Mount {
+ t.Fatal("persistent topology inspection after actual reboot", observation, err)
+ }
+ parentAfter, err := state.Read("namespace-parent.json")
+ 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")
if err := fs.CheckParked(&saved.Record); !errors.Is(err, hostbox.ErrChanged) {
t.Fatalf("old mount authority reused: %v", err)
}
diff --git a/internal/host/claimservingquarantine_linux.go b/internal/host/claimservingquarantine_linux.go
index 2b26a53e4..2e7f4a65d 100644
--- a/internal/host/claimservingquarantine_linux.go
+++ b/internal/host/claimservingquarantine_linux.go
@@ -169,7 +169,7 @@ func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingAr
return q, hostbox.ErrChanged
}
old := q.Previous != nil && reflect.DeepEqual(current, *q.Previous)
- fresh := q.Fresh.Inode != 0 && current == (servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh, BootID: q.BootID})
+ fresh := q.Fresh.Inode != 0 && current.Version == 1 && current.Parent == q.Parent && current.Root == q.Fresh && current.BootID == q.BootID
if !old && !fresh || q.Complete && !fresh {
return q, hostbox.ErrChanged
}
@@ -230,7 +230,12 @@ func (f *servingParkFS) recoverNamespace(ctx context.Context, expected servingAr
if err = f.sync(int(parent.Fd())); err != nil {
return q, err
}
- if err = ledger.write("namespace.json", servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh, BootID: q.BootID}); err != nil {
+ n := servingParkNamespace{Version: 1, Parent: q.Parent, Root: q.Fresh, BootID: q.BootID}
+ n.Topology, err = f.captureTopology(parent, fresh, n)
+ if err != nil {
+ return q, err
+ }
+ if err = ledger.write("namespace.json", n); err != nil {
return q, err
}
q.Complete = true
diff --git a/internal/host/claimservingtopology_linux.go b/internal/host/claimservingtopology_linux.go
new file mode 100644
index 000000000..c4de00cb0
--- /dev/null
+++ b/internal/host/claimservingtopology_linux.go
@@ -0,0 +1,199 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "encoding/hex"
+ "errors"
+ "os"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "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) {
+ var out servingParkPersistentDirectory
+ if d == nil {
+ return out, hostbox.ErrUnsupported
+ }
+ id, err := servingParkDirID(d)
+ if err != nil {
+ return out, err
+ }
+ var st unix.Stat_t
+ if err = unix.Fstat(int(d.Fd()), &st); err != nil {
+ return out, err
+ }
+ var fs unix.Statfs_t
+ if err = unix.Fstatfs(int(d.Fd()), &fs); err != nil {
+ return out, err
+ }
+ h, mount, err := unix.NameToHandleAt(int(d.Fd()), "", unix.AT_EMPTY_PATH)
+ if err != nil {
+ return out, err
+ }
+ if mount <= 0 || uint64(mount) != id.Mount {
+ return out, hostbox.ErrChanged
+ }
+ out = servingParkPersistentDirectory{Filesystem: fs.Fsid.Val, Type: h.Type(), Handle: hex.EncodeToString(h.Bytes()), Owner: st.Uid, Mode: st.Mode}
+ if !out.valid() {
+ return servingParkPersistentDirectory{}, hostbox.ErrChanged
+ }
+ return out, nil
+}
+
+// Capture only while the recorded boot and both descriptor identities still
+// match. Old namespaces are never upgraded by looking at their current paths.
+func (f *servingParkFS) captureTopology(parent, root *os.File, n servingParkNamespace) (*servingParkTopology, error) {
+ if f.handle == nil {
+ return nil, hostbox.ErrUnsupported
+ }
+ if err := f.checkBoot(n.BootID); err != nil {
+ return nil, err
+ }
+ p, err := servingParkDirID(parent)
+ if err != nil || p != n.Parent {
+ return nil, hostbox.ErrChanged
+ }
+ r, err := servingParkDirID(root)
+ if err != nil || r != n.Root || r.Device != p.Device || r.Mount != p.Mount {
+ return nil, hostbox.ErrChanged
+ }
+ ph, err := f.handle(parent)
+ if err != nil {
+ return nil, err
+ }
+ rh, err := f.handle(root)
+ if err != nil {
+ return nil, err
+ }
+ if !ph.valid() || !rh.valid() || ph.Filesystem != rh.Filesystem || rh.Owner != f.owner || rh.Mode&0077 != 0 {
+ return nil, hostbox.ErrChanged
+ }
+ p, err = servingParkDirID(parent)
+ if err != nil || p != n.Parent {
+ return nil, hostbox.ErrChanged
+ }
+ r, err = servingParkDirID(root)
+ if err != nil || r != n.Root || f.checkBoot(n.BootID) != nil {
+ return nil, hostbox.ErrChanged
+ }
+ return &servingParkTopology{Parent: ph, Root: rh}, nil
+}
+
+// This observation cannot be used by the park filesystem or sent to an agent.
+// A later recovery transaction must preserve retirement history and validate
+// every affected source/event before it can publish current mount authority.
+type servingParkRebootObservation struct {
+ Previous servingParkNamespace
+ Parent servingParkParent
+ BootID string
+ CurrentParent, CurrentRoot servingArtifactDirectory
+}
+
+// Called under the namespace lock. This is a read-only prerequisite for reboot
+// recovery, not permission to rewrite mount IDs or resume deletion. Quarantines
+// require a separate transition covering the displaced root and retained data.
+func (f *servingParkFS) inspectRebootTopology(ctx context.Context) (servingParkRebootObservation, error) {
+ var zero servingParkRebootObservation
+ if ctx == nil || f == nil || f.state == nil || f.handle == nil {
+ return zero, hostbox.ErrUnsupported
+ }
+ if err := ctx.Err(); err != nil {
+ return zero, err
+ }
+ boot, err := f.currentBoot()
+ if err != nil {
+ return zero, err
+ }
+ p, err := f.namespaceParent()
+ if err != nil {
+ return zero, err
+ }
+ var n servingParkNamespace
+ body, err := f.state.Read("namespace.json")
+ if err != nil {
+ return zero, err
+ }
+ if servingParkDecode(body, &n) != nil || n.Version != 1 || !validServingParkBootID(n.BootID) || n.BootID == boot || n.BootID != p.BootID || n.Parent != p.Parent || n.Topology == nil || !n.Topology.Parent.valid() || !n.Topology.Root.valid() || n.Root.Device != n.Parent.Device || n.Root.Mount != n.Parent.Mount || n.Root.Inode == 0 {
+ return zero, hostbox.ErrChanged
+ }
+ if _, err := f.state.Read("namespace-quarantine.json"); !errors.Is(err, os.ErrNotExist) {
+ if err == nil {
+ err = hostbox.ErrChanged
+ }
+ return zero, err
+ }
+ parent, pid, err := f.parent()
+ if err != nil {
+ return zero, err
+ }
+ defer parent.Close()
+ root, rid, err := f.privateDir(parent, ".serving-parked")
+ if err != nil {
+ return zero, err
+ }
+ defer root.Close()
+ // Device renumbering remains a refusal. Persistent evidence is additional
+ // to the original inode/device checks, never a reason to drop them.
+ if pid.Device != n.Parent.Device || pid.Inode != n.Parent.Inode || rid.Device != n.Root.Device || rid.Inode != n.Root.Inode || rid.Mount != pid.Mount {
+ return zero, hostbox.ErrChanged
+ }
+ current := servingParkNamespace{Version: 1, Parent: pid, Root: rid, BootID: boot}
+ topology, err := f.captureTopology(parent, root, current)
+ if err != nil {
+ return zero, err
+ }
+ if *topology != *n.Topology {
+ return zero, hostbox.ErrChanged
+ }
+ // The service user owns the mount parent. Reopen both names after reading
+ // the persistent handles; a detached original descriptor proves no path.
+ again, nextParent, err := f.parent()
+ if err != nil {
+ return zero, err
+ }
+ defer again.Close()
+ next, nextRoot, err := f.privateDir(again, ".serving-parked")
+ if err != nil {
+ return zero, err
+ }
+ defer next.Close()
+ if nextParent != pid || nextRoot != rid || f.checkBoot(boot) != nil {
+ return zero, hostbox.ErrChanged
+ }
+ againTopology, err := f.captureTopology(again, next, current)
+ if err != nil {
+ return zero, err
+ }
+ if *againTopology != *topology {
+ return zero, hostbox.ErrChanged
+ }
+ if err := ctx.Err(); err != nil {
+ return zero, err
+ }
+ return servingParkRebootObservation{Previous: n, Parent: p, BootID: boot, CurrentParent: pid, CurrentRoot: rid}, nil
+}
diff --git a/internal/host/claimservingtopology_linux_test.go b/internal/host/claimservingtopology_linux_test.go
new file mode 100644
index 000000000..fecdb8760
--- /dev/null
+++ b/internal/host/claimservingtopology_linux_test.go
@@ -0,0 +1,307 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "golang.org/x/sys/unix"
+)
+
+func servingTopologyFixture(t *testing.T) (*servingRootFixture, servingParkNamespace) {
+ t.Helper()
+ f := newServingRootFixture(t, true)
+ if _, err := f.park(); err != nil {
+ t.Fatal(err)
+ }
+ body, err := f.disk.Read("namespace.json")
+ var n servingParkNamespace
+ if err != nil || servingParkDecode(body, &n) != nil || n.Topology == nil {
+ t.Fatal("persistent topology was not captured", err)
+ }
+ return f, n
+}
+
+func TestServingParkPersistentDirectory(t *testing.T) {
+ d, err := os.Open(t.TempDir())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ if err := d.Chmod(0700); err != nil {
+ t.Fatal(err)
+ }
+ p, err := servingParkPersistentDir(d)
+ if err != nil || !p.valid() {
+ var st unix.Stat_t
+ var fs unix.Statfs_t
+ _ = unix.Fstat(int(d.Fd()), &st)
+ _ = unix.Fstatfs(int(d.Fd()), &fs)
+ h, m, e := unix.NameToHandleAt(int(d.Fd()), "", unix.AT_EMPTY_PATH)
+ id, ie := servingParkDirID(d)
+ t.Fatal("kernel persistent directory", p, err, "mode", st.Mode, "fsid", fs.Fsid, "handle", h.Type(), h.Bytes(), m, e, "statx", id, ie)
+ }
+ other, err := servingParkPersistentDir(d)
+ if err != nil || other != p {
+ t.Fatal("same descriptor changed identity", err)
+ }
+ for _, fault := range []string{"empty", "oversize", "uppercase", "filesystem", "type", "file", "writable"} {
+ t.Run(fault, func(t *testing.T) {
+ bad := p
+ switch fault {
+ case "empty":
+ bad.Handle = ""
+ case "oversize":
+ bad.Handle = strings.Repeat("aa", 129)
+ case "uppercase":
+ bad.Handle = "AA"
+ case "filesystem":
+ bad.Filesystem = [2]int32{}
+ case "type":
+ bad.Type = 0
+ case "file":
+ bad.Mode = unix.S_IFREG | 0700
+ case "writable":
+ bad.Mode |= 0020
+ }
+ if bad.valid() {
+ t.Fatal("invalid persistent identity accepted")
+ }
+ })
+ }
+ if _, err := servingParkPersistentDir(nil); !errors.Is(err, hostbox.ErrUnsupported) {
+ t.Fatal("nil descriptor accepted", err)
+ }
+}
+
+func TestServingParkRebootTopologyReadOnly(t *testing.T) {
+ f, n := servingTopologyFixture(t)
+ fs := f.p.ledger.fs.(*servingParkFS)
+ fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ before := servingBootSnapshot(t, f.disk.dir)
+ claim := f.f.read()
+ var got servingParkRebootObservation
+ err := f.p.ledger.withNamespace(t.Context(), func() error {
+ var err error
+ got, err = fs.inspectRebootTopology(t.Context())
+ return err
+ })
+ if err != nil || got.BootID != servingOtherBoot || !reflect.DeepEqual(got.Previous, n) || got.CurrentParent != n.Parent || got.CurrentRoot != n.Root {
+ t.Fatal("read-only topology observation", got, err)
+ }
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) || !reflect.DeepEqual(claim, f.f.read()) {
+ t.Fatal("inspection changed private authority or claim")
+ }
+ // Recognizing the root grants no authority over its events. The original
+ // namespace still belongs to the old boot, even after successful inspection.
+ if err := fs.recoverRecordedNamespace(t.Context()); err == nil {
+ t.Fatal("observation enabled namespace recovery")
+ }
+ if _, err := f.park(); err == nil {
+ t.Fatal("observation enabled parking")
+ }
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
+ t.Fatal("failed recovery rewrote retirement history")
+ }
+}
+
+func TestServingParkRebootTopologyRefusesReplacements(t *testing.T) {
+ for _, fault := range []string{"old-format", "malformed-boot", "same-boot", "parent-boot", "parent-record", "root-record", "filesystem", "parent-handle", "root-handle", "type", "owner", "mode", "root", "parent", "root-symlink", "mount", "read-error", "unsupported", "missing-reader", "quarantine", "corrupt-quarantine", "cancel", "root-race", "parent-race", "boot-race", "mode-race", "cancel-race"} {
+ t.Run(fault, func(t *testing.T) {
+ f, n := servingTopologyFixture(t)
+ fs := f.p.ledger.fs.(*servingParkFS)
+ fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ path := filepath.Join(fs.data, "instances", ".serving-parked")
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ switch fault {
+ case "old-format":
+ n.Topology = nil
+ case "malformed-boot":
+ n.BootID = "invalid"
+ p, err := fs.namespaceParent()
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.BootID = n.BootID
+ if err := f.p.ledger.write("namespace-parent.json", p); err != nil {
+ t.Fatal(err)
+ }
+ case "same-boot":
+ fs.boot = func() (string, error) { return n.BootID, nil }
+ case "parent-boot":
+ n.BootID = "11234567-89ab-4cde-8fab-0123456789ab"
+ case "parent-record":
+ n.Parent.Inode++
+ case "root-record":
+ n.Root.Inode++
+ case "filesystem":
+ n.Topology.Parent.Filesystem[0]++
+ n.Topology.Root.Filesystem = n.Topology.Parent.Filesystem
+ case "parent-handle":
+ n.Topology.Parent.Handle = "aa"
+ case "root-handle":
+ n.Topology.Root.Handle = "aa"
+ case "type":
+ n.Topology.Root.Type++
+ case "owner":
+ n.Topology.Parent.Owner++
+ case "mode":
+ n.Topology.Parent.Mode ^= 0100
+ case "root", "parent", "root-symlink":
+ target := path
+ if fault == "parent" {
+ target = filepath.Dir(path)
+ }
+ if err := os.Rename(target, target+"-old"); err != nil {
+ t.Fatal(err)
+ }
+ if fault == "root-symlink" {
+ if err := os.Symlink(target+"-old", target); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := os.Mkdir(target, 0700); err != nil {
+ t.Fatal(err)
+ }
+ case "mount":
+ fs.mount = func(string) (bool, error) { return false, nil }
+ case "read-error", "unsupported":
+ fs.handle = func(*os.File) (servingParkPersistentDirectory, error) {
+ if fault == "unsupported" {
+ return servingParkPersistentDirectory{}, unix.EOPNOTSUPP
+ }
+ return servingParkPersistentDirectory{}, unix.EIO
+ }
+ case "missing-reader":
+ fs.handle = nil
+ case "quarantine", "corrupt-quarantine":
+ body := []byte("{}")
+ if fault == "corrupt-quarantine" {
+ body = []byte("broken")
+ }
+ if err := f.disk.Write("namespace-quarantine.json", body); err != nil {
+ t.Fatal(err)
+ }
+ case "cancel":
+ cancel()
+ case "root-race", "parent-race", "boot-race", "mode-race", "cancel-race":
+ calls := 0
+ fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
+ p, err := servingParkPersistentDir(d)
+ calls++
+ if calls == 2 {
+ if fault == "cancel-race" {
+ cancel()
+ } else if fault == "mode-race" {
+ if err := os.Chmod(filepath.Dir(path), 0777); err != nil {
+ t.Fatal(err)
+ }
+ } else if fault == "boot-race" {
+ fs.boot = func() (string, error) { return n.BootID, nil }
+ } else {
+ target := path
+ if fault == "parent-race" {
+ target = filepath.Dir(path)
+ }
+ if err := os.Rename(target, target+"-old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(target, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ return p, err
+ }
+ }
+ if err := f.p.ledger.write("namespace.json", n); err != nil {
+ t.Fatal(err)
+ }
+ before := servingBootSnapshot(t, f.disk.dir)
+ if got, err := fs.inspectRebootTopology(ctx); err == nil || !reflect.DeepEqual(got, servingParkRebootObservation{}) {
+ t.Fatal("unsafe topology accepted", got, err)
+ }
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
+ t.Fatal("refusal changed authority")
+ }
+ })
+ }
+}
+
+func TestServingParkTopologyCaptureFailureRecovery(t *testing.T) {
+ for _, boundary := range []string{"handle", "namespace-before", "namespace-after"} {
+ t.Run(boundary, func(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ fs := f.p.ledger.fs.(*servingParkFS)
+ if boundary == "handle" {
+ fs.handle = func(*os.File) (servingParkPersistentDirectory, error) {
+ return servingParkPersistentDirectory{}, unix.EIO
+ }
+ } else {
+ fail := func(name string, body []byte) error {
+ if name == "namespace.json" {
+ return unix.EIO
+ }
+ return nil
+ }
+ store := quarantineWriteFault{Store: f.disk}
+ if boundary == "namespace-before" {
+ store.before = fail
+ } else {
+ store.after = fail
+ }
+ fs.state = store
+ }
+ if _, err := f.park(); !errors.Is(err, unix.EIO) {
+ t.Fatal("missing capture boundary", err)
+ }
+ if body, err := os.ReadFile(f.db); err != nil || len(body) == 0 {
+ t.Fatal("capture failure changed source", err)
+ }
+ fs.handle, fs.state = servingParkPersistentDir, f.disk
+ if _, err := f.park(); err != nil {
+ t.Fatal("capture recovery", err)
+ }
+ body, err := f.disk.Read("namespace.json")
+ var n servingParkNamespace
+ if err != nil || servingParkDecode(body, &n) != nil || n.Topology == nil {
+ t.Fatal("recovery lost topology", err)
+ }
+ })
+ }
+}
+
+func TestServingParkTopologyNeverUpgradesOldNamespace(t *testing.T) {
+ f, n := servingTopologyFixture(t)
+ fs := f.p.ledger.fs.(*servingParkFS)
+ n.Topology = nil
+ if err := f.p.ledger.write("namespace.json", n); err != nil {
+ t.Fatal(err)
+ }
+ before := servingBootSnapshot(t, f.disk.dir)
+ root, same, err := fs.namespace()
+ if err != nil {
+ t.Fatal("same-boot old namespace became unreadable", err)
+ }
+ root.Close()
+ if same.Topology != nil {
+ t.Fatal("old namespace gained inferred topology")
+ }
+ if err := fs.recoverRecordedNamespace(t.Context()); err != nil {
+ t.Fatal("same-boot old namespace recovery", err)
+ }
+ fs.boot = func() (string, error) { return servingOtherBoot, nil }
+ if _, err := fs.inspectRebootTopology(t.Context()); err == nil {
+ t.Fatal("old namespace gained cross-boot proof")
+ }
+ if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
+ t.Fatal("old namespace was rewritten")
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 3abc876eb..3fe6cc933 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 19 overlay; pending Paul’s census ruling.
+# Files seed checkpoint 20 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.
@@ -31,7 +31,7 @@ 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 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 333b3529ffbe801730ec31a62ff96d9b5c6ba931f00c79a34f00f46de2c1fccc
+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 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
@@ -40,13 +40,13 @@ internal/host/claimservingpark_linux.go servingParkFS.Delete authority C,J4 rev3
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.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 file:Read@282 0fc681872fb332c569c2164a6089ab590d4274bdb021800b97b0fdeed86ad6f6
+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_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 69baa4c747e687578e73b8369f2df9987fe6fb5836b78796cc41c776ba12c802
+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.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
@@ -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 5f5aa5f26594bf4db662e0da8b386341c966101242b6bdcd2af7fe1d936b4134
+@coverage internal/host 7ab31702b7dc1f909f0c3feb31042727737b908618992b5f3a51facc00480285
@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
@@ -156,7 +156,7 @@ internal/hostguard/coldhandover.go Guard.beginColdHandover authority C,J4 rev3=P
internal/host/claimservinghandover.go servingPriorRevision authority C,J4 rev3=PROPOSED-seed; canonical recorded directory revision bounded by the current authenticated enrollment revision; never rewrites retained stop evidence 8007e188e2ad600cf36d8eed0fb5adb591f08f52be52285c85db348e0780cb84
internal/host/claimservingquarantine_linux.go servingParkFS.checkQuarantine authority C,J4 rev3=PROPOSED-seed; no-follow descriptor identity of the root-chosen quarantine basename; does not inspect children or authorize deletion bbf7d5600fc6fcf0c60d2b7c9e83d8ac57f9edaca3367c9c6e3fadffab303d8d
internal/host/claimservingquarantine_linux.go servingParkFS.moveQuarantine authority C,J4 rev3=PROPOSED-seed; recorded obstruction device, inode and mount before and after same-mount NOREPLACE rename; lost-reply replay requires exact destination identity 12801fc3c906d28c41919fed8eb425dd01a480c42c7cd51653f686dc1f76a794
-internal/host/claimservingquarantine_linux.go servingParkFS.recoverNamespace authority C,J4 rev3=PROPOSED-seed; independently recorded parent, effective mount and strict private quarantine records; retains one obstruction plus one unrecorded replacement; completed replay is read-only; no content deletion; parent, prior namespace and quarantine boot identities must match the current boot; no mount-number refresh file:Fstat@454,file:Read@1075,file:Read@1391,file:Read@1986,file:Read@2327,file:Readdirnames@3973 9624383d3a20c040fa9dceacf13b04c2f5f3cfe7d19528f0318c02065c37ce95
+internal/host/claimservingquarantine_linux.go servingParkFS.recoverNamespace authority C,J4 rev3=PROPOSED-seed; independently recorded parent, effective mount and strict private quarantine records; retains one obstruction plus one unrecorded replacement; completed replay is read-only; no content deletion; parent, prior namespace and quarantine boot identities must match the current boot; no mount-number refresh; replacement namespace captures its own persistent proof; previous proof remains immutable file:Fstat@454,file:Read@1075,file:Read@1391,file:Read@1986,file:Read@2327,file:Readdirnames@3984 aab5a7073809ff4d48a1749c952114a62d6ae45d44a8b1147c4df948917ee9ab
internal/host/claimservingquarantine_linux.go servingParkQuarantine.name authority C,J4 rev3=PROPOSED-seed; root-generated 128-bit quarantine identity converted to a fixed basename; no caller pathname 08554ada2338c42eb0c53d7b39709583f6af12e51f5a005f42dc3bd44906b4ee
internal/host/claimservingquarantine_linux.go servingParkQuarantine.validate authority C,J4 rev3=PROPOSED-seed; private quarantine shape and phase checks; original, prior, unrecorded and fresh directories must have distinct same-mount identities; retention only; prior namespace and quarantine must carry the same boot scope 285f4c277e15af5001ff7d69723d1ff84a6137c8d08bedf5a7c8580d2e246398
internal/host/claimservingquarantine_linux.go type servingParkQuarantine authority C,J4 rev3=PROPOSED-seed; private versioned quarantine intent; additive unrecorded-replacement identity and rename progress bounded to one spare; no artifact deletion authority; additive BootID scopes parent, obstruction, prior root and replacement identities 602321aa523aa4d4b9bade287b70abf2c86f4daeb6bb245bc7f5412b688a1680
@@ -177,8 +177,16 @@ internal/host/claimservingdisplaced_linux.go servingParkFS.recordEventRoot autho
internal/host/claimservingcollection.go servingParkLedger.collectClosedLocked authority C,J4 rev3=PROPOSED-seed; private completed retirement, durable replay deadline, slot references and legacy-consumption evidence; preserve referenced records, mark permanent legacy consumption, then remove only obsolete private record and lock after 24 hours; no data deletion authority 97201d6a0b793715b5e8c0c0abb36d1c303e3b38f3d0e1a97aadd49063735984
internal/host/claimservingpark_records.go type servingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; permanent legacy source and authorization evidence; additive Collected flag distinguishes garbage collection from interrupted first-intent creation 1f7d2cd2baaad00efda56e7c5637e8b386d80ad7e5f2b311487ebcf109c4861b
internal/host/claimservingboot_linux.go servingParkBootID authority C,J4 rev3=PROPOSED-seed; bounded kernel boot-ID read from /proc; no caller-supplied path or identity file:Open@54,file:ReadAll@162 af8e5641ef60fcd307dec013df6eba3e50a29d2cb811c5b2cddf10c5c9404e30
-internal/host/claimservingboot_linux.go servingParkFS.currentBoot authority C,J4 rev3=PROPOSED-seed; production boot reader and canonical nonzero UUID validation; missing or unreadable boot evidence refuses fbc9dde6b4b8c3a7135f145e7544639be65779cb0253f4d47206522076a2f2e5
+internal/host/claimservingboot_linux.go servingParkFS.currentBoot authority C,J4 rev3=PROPOSED-seed; production boot reader and canonical nonzero UUID validation; missing or unreadable boot evidence refuses f6d671a41210250791f421efcf168fa98e3c2b4bd8b6aa314e9664261fd2b034
internal/host/claimservingboot_linux.go servingParkFS.checkBoot authority C,J4 rev3=PROPOSED-seed; private namespace boot identity compared with the current kernel boot; missing old identity or a reboot refuses without refreshing mount numbers 3d380a2fa685f3be883f9c595a01ee437d51597a1b05ab09fb9c50dfead2cac1
-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 9c4690a2490610e64ceb7a472f213e8abd9457e5c27ecca3916484d60268d43e
-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 44839ce2b07d361ab9d39f6063be879026f184071832a0848191397d76705320
+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/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/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