carlos / platform Public

WIP files seed checkpoint 28 (not for merge)

Delegated one-shot publishes and reruns; all 30 plain root tests pass together; Task 7 strings split ship/deferred with the activation runbook step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 10a77ee93603e713dd29397999345cb0f6f07a38 parent 2a8d29f
13 files changed, +260 −26
  • internal/host/claimfilesseed.go +1 −0
  • internal/host/claimfilesseed_cli_rootrunner_linux_test.go +22 −12
  • internal/host/claimfilesseed_integration_linux_test.go +30 −0
  • internal/host/claimfilesseed_isolated_rootrunner_linux_test.go +3 −0
  • internal/host/claimfilesseed_lifetime_rootrunner_linux_test.go +5 −0
  • internal/host/claimfilesseed_reboot_rootrunner_linux_test.go +85 −2
  • internal/host/claimfilesseed_stage.go +15 −0
  • internal/host/claimservingartifact_linux.go +9 −0
  • internal/host/claimservingartifact_other.go +4 −0
  • internal/host/claimservingpark_recovery.go +2 −2
  • internal/host/claimservingretire.go +18 −3
  • internal/host/claimservingretire_linux_test.go +57 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +9 −7

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	280.703s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.666s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.259s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.713s
ok  	github.com/carlosframework/platform/internal/activator	248.738s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.446s
ok  	github.com/carlosframework/platform/internal/apperr	0.080s
ok  	github.com/carlosframework/platform/internal/applog	0.120s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.586s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.015s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.703s
ok  	github.com/carlosframework/platform/internal/changelog	0.303s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	385.772s
?   	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.093s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.436s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.599s
?   	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.129s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.031s
ok  	github.com/carlosframework/platform/internal/host	160.365s
ok  	github.com/carlosframework/platform/internal/hostactors	0.356s
ok  	github.com/carlosframework/platform/internal/hostbox	2.335s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.473s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	4.956s
ok  	github.com/carlosframework/platform/internal/hostfence	0.039s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.564s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	1.015s
ok  	github.com/carlosframework/platform/internal/hostops	1.570s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.012s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.012s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.414s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.140s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.012s
ok  	github.com/carlosframework/platform/internal/infraview	0.013s
ok  	github.com/carlosframework/platform/internal/instancectl	4.195s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.707s
ok  	github.com/carlosframework/platform/internal/instancefs	0.016s
ok  	github.com/carlosframework/platform/internal/instancekey	0.014s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.018s
ok  	github.com/carlosframework/platform/internal/layout	1.498s
ok  	github.com/carlosframework/platform/internal/lease	9.800s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.017s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.153s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.520s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.061s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	20.960s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.762s
ok  	github.com/carlosframework/platform/internal/objstore	0.171s
ok  	github.com/carlosframework/platform/internal/ops	0.178s
ok  	github.com/carlosframework/platform/internal/park	0.197s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	16.914s
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.988s
ok  	github.com/carlosframework/platform/internal/relay	7.242s
ok  	github.com/carlosframework/platform/internal/release	19.620s
ok  	github.com/carlosframework/platform/internal/rollout	3.792s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.006s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/runs	0.580s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.464s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.534s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.669s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.195s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.557s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 46989474 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/15 18:42:55 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC827FC8DDE18, 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.00s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.019s
minio contract: PASS
diff --git a/internal/host/claimfilesseed.go b/internal/host/claimfilesseed.go
index 880e912ba..feba7abe6 100644
--- a/internal/host/claimfilesseed.go
+++ b/internal/host/claimfilesseed.go
@@ -251,6 +251,7 @@ func (e *filesSeedExecutor) host(ctx context.Context, o hostclaim.Observed, out
return err
}
admitted, settled, err := prepareFilesSeed(ctx, *row, runtime, o, token)
+ err = errors.Join(err, op.reportSkipped(out, o.Host))
if !settled {
e.pending[o.Host] = filesSeedPending{exclusion: token, wake: wake}
}
diff --git a/internal/host/claimfilesseed_cli_rootrunner_linux_test.go b/internal/host/claimfilesseed_cli_rootrunner_linux_test.go
index 97ca3440a..e079bfd05 100644
--- a/internal/host/claimfilesseed_cli_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_cli_rootrunner_linux_test.go
@@ -86,8 +86,30 @@ func rootFilesSeedDelegatedCLI(t *testing.T, binary string) {
f.f.kernel.noProcess = true
f.p.root.Stop.Coordinator.Kernel = f.f.kernel
f.p.root.Stop.Coordinator.RetainNoProcess = f.p.retainLegacyNoProcess
+ reg, err := registry.Open("/data/registry.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { reg.Close() })
+ row, err := executor.registry.Get(recoveryHost)
+ if err != nil || row == nil {
+ t.Fatal(err)
+ }
+ owned, err := f.p.root.Reconcile.Registry.Get(recoveryHost)
+ if err != nil || owned == nil || !owned.ClaimOwnership.Valid() {
+ t.Fatal("missing root-owned fixture route", err)
+ }
+ row.ClaimOwnership = owned.ClaimOwnership
+ if err := reg.Upsert(*row); err != nil {
+ t.Fatal(err)
+ }
+ // Production root and agent observe the same ownership transfer.
+ f.p.root.Reconcile.Registry = reg
h := servingTransportRoot(t, f)
+ containmentRoot := claimContainmentAgentRoot
capability := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: f.actor.Box}).root
+ // The capability fixture assumes system.slice; this installed edge uses carlos.slice.
+ claimContainmentAgentRoot = containmentRoot
capability.Authenticate = h.Stop.Authenticate
capability.SeedController = verifyFilesSeedDelegation
listener, err := net.Listen("unix", controlsock.Path)
@@ -107,18 +129,6 @@ func rootFilesSeedDelegatedCLI(t *testing.T, binary string) {
t.Error(err)
}
})
- reg, err := registry.Open("/data/registry.db")
- if err != nil {
- t.Fatal(err)
- }
- row, err := executor.registry.Get(recoveryHost)
- if err != nil || row == nil {
- t.Fatal(err)
- }
- if err := reg.Upsert(*row); err != nil {
- t.Fatal(err)
- }
- reg.Close()
if err := os.MkdirAll(filepath.Dir(cfg.Executable), 0755); err != nil {
t.Fatal(err)
}
diff --git a/internal/host/claimfilesseed_integration_linux_test.go b/internal/host/claimfilesseed_integration_linux_test.go
index d696aac5c..6b6057683 100644
--- a/internal/host/claimfilesseed_integration_linux_test.go
+++ b/internal/host/claimfilesseed_integration_linux_test.go
@@ -7,6 +7,7 @@ import (
"context"
"os"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -212,6 +213,35 @@ func TestFilesSeedRealOriginPreparation(t *testing.T) {
runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller")
}
+func TestFilesSeedReportsSkippedPaths(t *testing.T) {
+ witness := filesSeedLiveWitness{seed: func(t *testing.T, p *servingPreparation, executor *filesSeedExecutor) {
+ host := p.f.f.hostName()
+ dir := filepath.Join(filepath.Dir(p.f.db), host)
+ large, err := os.Create(filepath.Join(dir, "large.bin"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = large.Truncate(hostfiles.MaxFileBytes + 1)
+ large.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("payload", filepath.Join(dir, "link\nname")); err != nil {
+ t.Fatal(err)
+ }
+ var output bytes.Buffer
+ if err := executor.run(t.Context(), host, &output); err != nil {
+ t.Fatalf("seed: %v\n%s", err, output.String())
+ }
+ for _, want := range []string{`skipped file "large.bin": exceeds 64 MiB`, `skipped file "link\nname": not a regular file.`, `: seeded.`} {
+ if !strings.Contains(output.String(), want) {
+ t.Fatalf("missing %q in output:\n%s", want, output.String())
+ }
+ }
+ }}
+ runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", nil, []filesSeedLiveWitness{witness})
+}
+
func TestFilesSeedPreparationChangedController(t *testing.T) {
witness := filesSeedLiveWitness{configure: func(*ClaimWakeAssembly) {}, serve: func(*testing.T, *servingPreparation, hostclaim.Observed, activator.ClaimStart) {}}
witness.prepare = func(p *servingPreparation) {
diff --git a/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
index 78608e3cf..eed1a8202 100644
--- a/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_isolated_rootrunner_linux_test.go
@@ -95,6 +95,9 @@ func rootFilesSeedIsolatedWitness(t *testing.T, binary string, twoHosts ...bool)
}
t.Cleanup(func() { os.RemoveAll(dir) })
}
+ if t.Name() == "TestRootRebootFilesSeedTwoHostPhase1" || t.Name() == "TestRootRebootFilesSeedTwoHostPhase2" {
+ seedRebootDataMount(t)
+ }
account, err := user.Lookup("carlos")
if err != nil {
t.Fatal(err)
diff --git a/internal/host/claimfilesseed_lifetime_rootrunner_linux_test.go b/internal/host/claimfilesseed_lifetime_rootrunner_linux_test.go
index 469c8dbdb..5dcd24565 100644
--- a/internal/host/claimfilesseed_lifetime_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_lifetime_rootrunner_linux_test.go
@@ -173,6 +173,11 @@ func rootFilesSeedLifetime(t *testing.T, p *servingPreparation, e *filesSeedExec
}
func TestFilesSeedExecutorLifetime(t *testing.T) {
+ // The runner's /tmp is a small tmpfs. Keep the real start-floor check,
+ // and place this direct fixture on its disk, as the installed /data tests do.
+ if os.Geteuid() == 0 {
+ t.Setenv("TMPDIR", "/var/tmp")
+ }
for _, mode := range []string{"Complete", "Disconnect", "Shutdown"} {
t.Run(mode, func(t *testing.T) {
runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", nil, []filesSeedLiveWitness{{seed: rootFilesSeedLifetime}})
diff --git a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
index e3a9c890b..a5664ce30 100644
--- a/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
+++ b/internal/host/claimfilesseed_reboot_rootrunner_linux_test.go
@@ -10,7 +10,9 @@ import (
"net"
"net/http/httptest"
"os"
+ "os/exec"
"path/filepath"
+ "reflect"
"sort"
"strconv"
"strings"
@@ -25,6 +27,7 @@ import (
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
)
const seedRebootPath = "/var/lib/carlos-rootrunner-files-seed-reboot"
@@ -173,6 +176,7 @@ func TestRootRebootFilesSeedTwoHostPhase2(t *testing.T) {
t.Cleanup(func() { state.Close() })
fs := newServingParkFS("/data", state)
ledger := &servingParkLedger{state: state, fs: fs, now: time.Now}
+ rebootRoot := &servingParkRoot{ledger: ledger}
for _, h := range saved.Hosts {
for _, r := range h.Records {
if err := ledger.fs.CheckParked(&r); err == nil {
@@ -181,10 +185,15 @@ func TestRootRebootFilesSeedTwoHostPhase2(t *testing.T) {
if !r.Complete {
t.Fatal("unfinished reboot fixture", h.Host)
}
- if err := ledger.withNamespace(t.Context(), func() error { return ledger.prepareRebootLocked(t.Context(), r, nil) }); err != nil {
+ if err := rebootRoot.prepareServingReboot(t.Context(), r.Authorization.ID); err != nil {
+ topology, topErr := fs.inspectRebootTopology(t.Context())
+ observation, inspectErr := fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
+ rawRecord, readErr := ledger.readHistorical(r.Authorization.ID)
+ slot, slotErr := ledger.slot(r.Authorization)
+ t.Logf("reboot diagnosis topology=%+v err=%v inspection=%+v err=%v savedMatches=%v readErr=%v slot=%+v slotErr=%v saved=%+v", topology, topErr, observation, inspectErr, reflect.DeepEqual(rawRecord, r), readErr, slot, slotErr, r)
t.Fatal("prepare completed park", h.Host, err)
}
- if err := ledger.withNamespace(t.Context(), func() error { return ledger.commitRebootLocked(t.Context(), r, nil) }); err != nil {
+ if err := rebootRoot.commitServingReboot(t.Context(), r.Authorization.ID); err != nil {
t.Fatal("recover park", h.Host, err)
}
}
@@ -292,3 +301,77 @@ func reopenSeedLiveHost(t *testing.T, saved seedRebootHost, witness filesSeedLiv
p := newServingPreparation(t, f, "boot", fmt.Sprintf("reboot-%d", os.Getpid()))
return &seedLiveHost{p: p, endpoint: server.URL, files: opened.(*movesFiles), witness: witness}
}
+
+// A grown root volume can receive different partition device numbers at its
+// first reboot. The production contract refuses device renumbering. Give this
+// reboot witness its own stable filesystem instead of relaxing that contract.
+func seedRebootDataMount(t *testing.T) {
+ t.Helper()
+ var root unix.Stat_t
+ if err := unix.Stat("/", &root); err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("runner root device before data mount: %d", root.Dev)
+ image := filepath.Join(seedRebootPath, "data.ext4")
+ deviceFile := filepath.Join(seedRebootPath, "data-device")
+ command := func(args ...string) string {
+ t.Helper()
+ out, err := exec.CommandContext(t.Context(), args[0], args[1:]...).CombinedOutput()
+ if err != nil {
+ t.Fatalf("reboot data command %q: %v %s", args, err, out)
+ }
+ return strings.TrimSpace(string(out))
+ }
+ var device string
+ if t.Name() == "TestRootRebootFilesSeedTwoHostPhase1" {
+ file, err := os.OpenFile(image, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = file.Truncate(2 << 30)
+ closeErr := file.Close()
+ if err != nil || closeErr != nil {
+ t.Fatal(err, closeErr)
+ }
+ command("mkfs.ext4", "-q", "-F", image)
+ device = command("losetup", "--find", "--show", image)
+ if err := os.WriteFile(deviceFile, []byte(device), 0600); err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ body, err := os.ReadFile(deviceFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ device = string(body)
+ number := strings.TrimPrefix(device, "/dev/loop")
+ if number == device || number == "" {
+ t.Fatal("invalid saved loop device", device)
+ }
+ if _, err := strconv.ParseUint(number, 10, 32); err != nil {
+ t.Fatal(err)
+ }
+ // losetup refuses a device already in use; never detach someone else's loop.
+ command("losetup", device, image)
+ }
+ t.Cleanup(func() {
+ cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+ if out, err := exec.CommandContext(cleanup, "losetup", "--detach", device).CombinedOutput(); err != nil {
+ t.Errorf("detach fixture loop: %v %s", err, out)
+ }
+ })
+ if err := unix.Mount(device, "/data", "ext4", unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := unix.Unmount("/data", 0); err != nil {
+ t.Error(err)
+ }
+ })
+ var data unix.Stat_t
+ if err := unix.Stat("/data", &data); err != nil {
+ t.Fatal(err)
+ }
+ t.Logf("persistent data device: %s (%d)", device, data.Dev)
+}
diff --git a/internal/host/claimfilesseed_stage.go b/internal/host/claimfilesseed_stage.go
index 94e7e4625..5e3bebbf6 100644
--- a/internal/host/claimfilesseed_stage.go
+++ b/internal/host/claimfilesseed_stage.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"errors"
+ "fmt"
"io"
"os"
"strings"
@@ -29,6 +30,20 @@ type filesSeedOperation struct {
stats hostfiles.SyncStats
}
+func (s *filesSeedOperation) reportSkipped(out io.Writer, host string) error {
+ for _, path := range s.stats.TooLarge {
+ if _, err := fmt.Fprintf(out, "Host %q: skipped file %q: exceeds 64 MiB and will not be restored on a later cold wake.\n", host, path); err != nil {
+ return err
+ }
+ }
+ for _, path := range s.stats.NotRegular {
+ if _, err := fmt.Fprintf(out, "Host %q: skipped file %q: not a regular file.\n", host, path); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// The private provider is freshly opened. No live provider or mutex-bearing
// owner is copied or modified. The command binds admission before materializing.
func newFilesSeedProvider(ctx context.Context, source *movesFilesSource, claims release.Store, data lease.VersionedStore, actor hostbox.Actor, reg *registry.Registry, selected filesSeedSelection) (*movesFiles, *filesSeedOperation, error) {
diff --git a/internal/host/claimservingartifact_linux.go b/internal/host/claimservingartifact_linux.go
index 54b22c879..2332cdb7d 100644
--- a/internal/host/claimservingartifact_linux.go
+++ b/internal/host/claimservingartifact_linux.go
@@ -39,3 +39,12 @@ func checkServingArtifactAbsence(db string, expected servingArtifactDirectory) e
}
return nil
}
+
+func servingAgentDirectory(db string) (servingArtifactDirectory, error) {
+ dir, err := instancefs.OpenDirBeneath(filepath.Dir(db), ".")
+ if err != nil {
+ return servingArtifactDirectory{}, err
+ }
+ defer dir.Close()
+ return servingParkDirID(dir)
+}
diff --git a/internal/host/claimservingartifact_other.go b/internal/host/claimservingartifact_other.go
index a69c868f3..6bb477a39 100644
--- a/internal/host/claimservingartifact_other.go
+++ b/internal/host/claimservingartifact_other.go
@@ -7,3 +7,7 @@ import "github.com/carlosframework/platform/internal/hostbox"
func checkServingArtifactAbsence(string, servingArtifactDirectory) error {
return hostbox.ErrUnsupported
}
+
+func servingAgentDirectory(string) (servingArtifactDirectory, error) {
+ return servingArtifactDirectory{}, hostbox.ErrUnsupported
+}
diff --git a/internal/host/claimservingpark_recovery.go b/internal/host/claimservingpark_recovery.go
index b0e594a4f..238944d89 100644
--- a/internal/host/claimservingpark_recovery.go
+++ b/internal/host/claimservingpark_recovery.go
@@ -14,7 +14,7 @@ import (
const servingParkRecoveryLimit = 128
-var errServingParkRetagPending = errors.New("the earlier park is incomplete; the next wake or root service restart completes it before a new park")
+var errServingParkRetagPending = errors.New("the earlier park is incomplete; retain its evidence and inspect the root service journal before retrying")
type servingParkRecoveryResult struct {
Host, ID string
@@ -144,7 +144,7 @@ func (p *servingParkRuntime) recoverPending(ctx context.Context) error {
for _, result := range page.Results {
if result.Err != nil {
// Draft retirement.reap_failed and reason.park_pending from the index.
- reason := "a local park operation failed; inspect the park listing and root service journal"
+ reason := "a local park operation failed; inspect the root service journal"
if errors.Is(result.Err, errServingParkRetagPending) {
reason = errServingParkRetagPending.Error()
} else if errors.Is(result.Err, hostbox.ErrChanged) || errors.Is(result.Err, ErrClaimStopReceipt) {
diff --git a/internal/host/claimservingretire.go b/internal/host/claimservingretire.go
index ce954ab71..11d0d0f29 100644
--- a/internal/host/claimservingretire.go
+++ b/internal/host/claimservingretire.go
@@ -185,6 +185,10 @@ func (r *claimServingArtifactRetirement) recover(ctx context.Context, admitted h
return err
}
}
+ local, err := servingAgentDirectory(in.DBPath)
+ if err != nil {
+ return err
+ }
authorization, err := r.Root.Park(ctx, current, prior)
if err != nil {
return err
@@ -192,7 +196,7 @@ func (r *claimServingArtifactRetirement) recover(ctx context.Context, admitted h
if generation != "" && authorization.Predecessor.Generation != generation {
return hostbox.ErrChanged
}
- return r.finish(ctx, admitted, in, authorization, nil)
+ return r.finish(ctx, admitted, in, authorization, local, nil)
}
func (r *claimServingArtifactRetirement) BeforeLegacyInstall(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
@@ -211,6 +215,10 @@ func (r *claimServingArtifactRetirement) BeforeLegacyInstall(ctx context.Context
if (prior.Session == 0) != (prior.Generation == "") || prior.Session >= current.Session || (prior.Empty && prior.Session != 0) {
return hostbox.ErrChanged
}
+ local, err := servingAgentDirectory(in.DBPath)
+ if err != nil {
+ return err
+ }
authorization, err := r.Root.ParkLegacy(ctx, current, prior)
if err != nil {
return err
@@ -218,7 +226,7 @@ func (r *claimServingArtifactRetirement) BeforeLegacyInstall(ctx context.Context
if !authorization.Legacy {
return hostbox.ErrChanged
}
- return r.finish(ctx, admitted, in, authorization, work)
+ return r.finish(ctx, admitted, in, authorization, local, work)
}
func (r *claimServingArtifactRetirement) validateAuthorization(ctx context.Context, fresh hostclaim.Observed, current servingArtifactIdentity, a servingParkAuthorization) error {
@@ -263,7 +271,7 @@ func (r *claimServingArtifactRetirement) validateAuthorization(ctx context.Conte
return nil
}
-func (r *claimServingArtifactRetirement) finish(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization, work hostwrite.Work) error {
+func (r *claimServingArtifactRetirement) finish(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization, local servingArtifactDirectory, work hostwrite.Work) error {
fresh, current, _, _, err := r.snapshot(ctx, admitted, in)
if err != nil {
return err
@@ -271,6 +279,13 @@ func (r *claimServingArtifactRetirement) finish(ctx context.Context, admitted ho
if err = r.validateAuthorization(ctx, fresh, current, a); err != nil {
return err
}
+ // Mount IDs belong to a namespace. Root keeps its original mount identity;
+ // the hardened agent pins its own mount before the RPC and checks it again
+ // during enrollment. Both views must still name the same device and inode.
+ if a.Directory.Device != local.Device || a.Directory.Inode != local.Inode || local.Mount == 0 {
+ return hostbox.ErrChanged
+ }
+ a.Directory = local
enroll := func() error { return r.enrollPairLocked(ctx, admitted, in, a) }
if work != nil {
err = work.With(ctx, in.Identity, enroll)
diff --git a/internal/host/claimservingretire_linux_test.go b/internal/host/claimservingretire_linux_test.go
index 995368673..743262d4f 100644
--- a/internal/host/claimservingretire_linux_test.go
+++ b/internal/host/claimservingretire_linux_test.go
@@ -765,3 +765,60 @@ func TestServingRetirementFailedReplayInvalidatesPermission(t *testing.T) {
t.Fatal("old completion survived failed replay")
}
}
+
+// Root and the hardened edge see the same inode through different mounts.
+type servingNamespaceParker struct {
+ servingDatabaseParker
+ change func(*servingParkAuthorization)
+}
+
+func (p servingNamespaceParker) Park(ctx context.Context, current, prior servingArtifactIdentity) (servingParkAuthorization, error) {
+ a, err := p.servingDatabaseParker.Park(ctx, current, prior)
+ p.change(&a)
+ return a, err
+}
+
+func (p servingNamespaceParker) ParkLegacy(ctx context.Context, current servingArtifactIdentity, prior servingLegacyPredecessor) (servingParkAuthorization, error) {
+ a, err := p.servingDatabaseParker.ParkLegacy(ctx, current, prior)
+ p.change(&a)
+ return a, err
+}
+func TestServingEnrollmentMountNamespaces(t *testing.T) {
+ for _, kind := range []string{"namespace", "device", "inode", "replacement"} {
+ t.Run(kind, func(t *testing.T) {
+ f, r, p, in := servingRetirementFixture(t)
+ admitted := servingAdmit(t, f, &in, "G")
+ r.Root = servingNamespaceParker{p, func(a *servingParkAuthorization) {
+ a.Directory.Mount++
+ switch kind {
+ case "device":
+ a.Directory.Device++
+ case "inode":
+ a.Directory.Inode++
+ case "replacement":
+ dir := filepath.Dir(in.DBPath)
+ if err := os.Rename(dir, dir+".old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }}
+ err := r.BeforeLegacyInstall(t.Context(), admitted, in, servingRetirementWork{p, r, admitted})
+ if kind == "namespace" {
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := r.EnrollLocked(t.Context(), admitted, in); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.Recover(t.Context(), admitted, in); err != nil {
+ t.Fatal("namespace recovery", err)
+ }
+ } else if err == nil {
+ t.Fatal("changed source accepted")
+ }
+ })
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 090a298df..a0c1f85a7 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -31,7 +31,7 @@ internal/host/agent.go runAgent authority B,C,D,F,J4 rev3=PROPOSED-seed; agent s
internal/host/claimassembly.go ClaimAssembly.Build authority C,G,J4 rev3=PROPOSED-seed; captured claim source, writer and runtime resources; construct fresh private serving owner without copying mutex-bearing live state file:Prepare@3640,file:Prepare@3657,file:Prepare@4369,file:Read@4299 42fc9bdc95130d62b0c5001821a924499dbe8cbf0d7e72d78e2f367978d0c8a6
internal/host/claimassembly.go NewProductionRootClaimStop authority C,J4 rev3=PROPOSED-seed; verified root assembly, actor and managed roots; binds private serving-park runtime and retained session-zero stop evidence; agent serving construction remains refused 7debdfe43ca6f17e2229c8aa8b566ee22112d86a595a66fb0cb0213253854aa8
internal/host/claimconstruction.go OpenProductionClaimAssembly authority C,G rev3=PROPOSED-seed; production serving retirement, enrollment and readiness bindings; early metadata and capacity guards before retry reservation file:Read@4150,file:Read@9908,file:ReadFile@3694,registry:Host@9935,whole:Route@10023,whole:Route@10772 60f3c8f0a32435e0341912069f33238793659b7059a2a5661308eaae8b08c6c7
-internal/host/claimfilesseed.go filesSeedExecutor.host authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join file:Exec@476,file:Exec@661,file:Open@1102,file:Read@2129,file:Read@3787,registry:Backing@974,registry:Replica@815,registry:Seed@956,registry:Tunnel@1032,registry:Unit@995,whole:Route@1383,whole:Route@2453,whole:Route@2735,whole:Route@3480,whole:Route@4570,whole:Route@4665,whole:Route@767 d49f4bbd7c5abe81d09b1c2b396b4546f3ea97f37c0e8f5e3952fbdb187fedb4
+internal/host/claimfilesseed.go filesSeedExecutor.host authority C,J4 rev3=PROPOSED-seed; existing selection and exclusion; report collected skipped paths after preparation, propagating output errors; no new authority or filesystem reads file:Exec@476,file:Exec@661,file:Open@1102,file:Read@2129,file:Read@3787,registry:Backing@974,registry:Replica@815,registry:Seed@956,registry:Tunnel@1032,registry:Unit@995,whole:Route@1383,whole:Route@2453,whole:Route@2735,whole:Route@3480,whole:Route@4570,whole:Route@4665,whole:Route@767 021e70ac6e298d1315425386258da392c408737ca39a6a924c440f05ab21fd41
internal/host/claimfilesseed.go filesSeedExecutor.run authority C,J4 rev3=PROPOSED-seed; local serving claims, registry, authoritative files evidence and ordinary inputs under one host exclusion; serial candidates, exact rerun settlement, one admitted attempt and private C8 without launch 017c0b7c89f6bfa337e294624b0edaeafc2d360369b6a3ddbf417045b71fc71f
internal/host/claimfilesseed.go filesSeedRetryReport diagnostic C,J4 rev3=PROPOSED-seed; incarnation-keyed missing-files diagnostics and indexed retry results; throttles output only and grants no retry, stop or launch c9f069dbde2eb1bfb7d98f8fcf1d949111d729d3dffa0dacc2a9591edb263193
internal/host/claimfilesseed.go type filesSeedExecutor authority C,J4 rev3=PROPOSED-seed; local serving claims, registry, authoritative files evidence and ordinary inputs under one host exclusion; serial candidates, exact rerun settlement, one admitted attempt and private C8 without launch 47b12d2f11ee4b5490b5ec5f6ffcf50100481fe063d5a53d960f3adce081e11b
@@ -204,10 +204,10 @@ internal/host/claimservingpark_records.go validateServingParkRecord authority C,
internal/host/claimservingpark_recovery.go const servingParkRecoveryLimit authority C,J4 rev3=PROPOSED-seed; fixed root-side processing limit of 128 retirement records per recovery page 6b949581334f55a7bae1b4579e56ac74810fc3042fdf568fbfa8e6b3c5a2ef11
internal/host/claimservingpark_recovery.go servingParkLedger.recoverCompleted authority C,J4 rev3=PROPOSED-seed; private completed retirement, exact filesystem deletion and slot references; resume removal, clear completed pending reference and collect only obsolete tombstones after durable replay deadline a2a277c862ec65e8abaf8b93b7eba77a79e538a76d061fb080f37f6c0c6d48f4
internal/host/claimservingpark_recovery.go servingParkLedger.recoveryIDs authority C,J4 rev3=PROPOSED-seed; private rootstate names; sorted bounded retirement page and continuation cursor; malformed record IDs retained for reporting 5b450cd0fb6ae6694bca2f9f75fe972c0d65681981a4caddde7e5c7269108782
-internal/host/claimservingpark_recovery.go servingParkRuntime.recoverPending authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated ac8d81393bd0eea077e0f8b29a402f3c13a0d2627b228be5271a4c6c7dab493b
+internal/host/claimservingpark_recovery.go servingParkRuntime.recoverPending authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated; draft diagnostic wording correction only; reads unchanged 67ccc5c3a30f7a3894cd5842a4ca3a9a9a0114f2f02faa0583fa8b220444919c
internal/host/claimservingpark_recovery.go type servingParkRecoveryPage authority C,J4 rev3=PROPOSED-seed; private bounded recovery results, cursor and incomplete indicator; never a transport selector c2fb4445ed50b5237b72e8a5fff9b1b2f6c9dd1397473eeb2c78a6b8c2804b27
internal/host/claimservingpark_recovery.go type servingParkRecoveryResult authority C,J4 rev3=PROPOSED-seed; root-derived host and record ID with local recovery outcome 4dd13c07e97470a6c5aa4fa98d326de8947422018f0d30d5c5af32d753cfcc39
-internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags 8fa87ebef4defae4fc1733528a76e0cc260711c2f5a664308e5368d69286d45b
+internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags; draft diagnostic wording correction only; reads unchanged 4ab3ce0ee055f4c01e8f6e12a5c1eed34bc3b4265adbc62b3bf9488fbb62e831
internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingPark authority C,J4 rev3=PROPOSED-seed; retained intent, exact current successor, independently rechecked installation, enrollment, receipt and local launch history; resumes original park and completes only existing paired tags; no retry settlement c02a2727bb45c39df280800ade0aaae6dfba18b268e48bea5fa2f7fcb7a2af23
internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingParks authority C,J4 rev3=PROPOSED-seed; census-ruling-FS-R6; root startup and minute worker recover immutable incomplete parks and reboot bindings; per-record refusal retained 87c40006c0aeb45e507a81c715f658f83100c07002d8fc5c5d53a1d87e70c196
internal/host/claimservingpark_recovery_linux.go servingParkRoot.validateServingRecoveryAuthority authority C,J4 rev3=PROPOSED-seed; fresh root history, original and continuation stop receipts, permanent legacy consumption, launch disposition and unlaunched active successor; extracted unchanged checks shared with same-boot startup 60ff31e49f8bafdf5a9c57b87ec8901e9ad3bafa6cc9e15d541a5659a83cf846
@@ -257,14 +257,14 @@ internal/host/claimservingreboot_prepare_linux.go servingParkRoot.prepareServing
internal/host/claimservingreboot_prepare_linux.go type servingParkRebootAuthority authority C,J4 rev3=PROPOSED-seed; current claim ETag, independently selected actor, directory and paired tags for an incomplete retirement; preparation only, never active mount authority 17bec014a0d01c9882d41727f6e9e97ad850104225c3c7f1f4c2ce09bc39bb34
internal/host/claimservingreboot_prepare_linux.go type servingParkRebootPreparation authority C,J4 rev3=PROPOSED-seed; private pending reboot evidence containing unchanged retirement, topology, slot and legacy history plus separately observed current authority; no filesystem or transport consumer a52a92a9b9d6507a2cbdffb1d8a76af3a541a023d421690ce588cf13a802c0d9
internal/host/claimservingretire.go bindServingArtifactRetirement authority C,J4 rev3=PROPOSED-seed; in-process root-client dependency and existing operation callbacks; serving dispatch does not replace landing/recovery dispatch 19ade85561b59f2f0b75f4f50b33333ab2598cba7d20e596893530c6a2f8102a
-internal/host/claimservingretire.go claimServingArtifactRetirement.BeforeLegacyInstall authority C,J4 rev3=PROPOSED-seed; admitted tuple and observed predecessor selection; root legacy authorization outside Work followed by guarded enrollment e8c23b09776dd4367e2a0fb5b85102747c3fe14cd45581f24e502e61b7fbcfd0
+internal/host/claimservingretire.go claimServingArtifactRetirement.BeforeLegacyInstall authority C,J4 rev3=PROPOSED-seed; admitted tuple and observed predecessor selection; root legacy authorization outside Work followed by guarded enrollment; agent mount anchored before root call and same device/inode bound across namespaces; root records unchanged 1295beaea78b831a9dc90a52608cc1abaa0fcc6224a0f499b2e615745595f901
internal/host/claimservingretire.go claimServingArtifactRetirement.EnrollLocked authority C,J4 rev3=PROPOSED-seed; completed root authorization cached for exact writer, fresh current tags and source absence; missing or stale permission refuses local mutation cf33600f5c3faaa0cde5658e53b1657ceadb7340244a68c9791441834a372729
internal/host/claimservingretire.go claimServingArtifactRetirement.Recover authority C,J4 rev3=PROPOSED-seed; admitted serving observation and launch inputs; resumes retirement without treating absent marker as enrollment authority 674a5a3c598cbdc29494a3cfc0859dd632c41be154a966ca7684f4440cbccdcb
internal/host/claimservingretire.go claimServingArtifactRetirement.Retire authority C,J4 rev3=PROPOSED-seed; captured predecessor generation; delegates only to serving recovery and root parking contract 6cf2e52f7e2aa511622268dd0a8dd37f1713e5beb4eba920e70057c2c90c32a3
internal/host/claimservingretire.go claimServingArtifactRetirement.enrollPairLocked authority C,J4 rev3=PROPOSED-seed; fresh claim and root-bound source directory; descriptor-relative absence before and after paired tag CAS; exact readback resolves uncertain CAS 351faf39946ade5f9166b79c0a177001c8b4287ee698ce37c2c1e8337b534c30
-internal/host/claimservingretire.go claimServingArtifactRetirement.finish authority C,J4 rev3=PROPOSED-seed; root authorization; guarded paired-tag update before root completion; in-memory final-install permission only after completion 4ecf52c799a177f0df88d0429eb5bffc8d485045be065eb0107d0821772b0a29
+internal/host/claimservingretire.go claimServingArtifactRetirement.finish authority C,J4 rev3=PROPOSED-seed; root authorization; guarded paired-tag update before root completion; in-memory final-install permission only after completion; agent mount anchored before root call and same device/inode bound across namespaces; root records unchanged 98b740f4463d8b287640cbd31204a94e6aeac01a4ff006afeac546f07ac11190
internal/host/claimservingretire.go claimServingArtifactRetirement.forgetCompletion authority C,J4 rev3=PROPOSED-seed; in-memory completed authorization for the selected host; failed replay must invalidate prior final-install permission 771a11b74e74ca98cbb51a1da72ecffea91801c2b622448a559eb1bdc6f7b3b5
-internal/host/claimservingretire.go claimServingArtifactRetirement.recover authority C,J4 rev3=PROPOSED-seed; fresh paired tags and predecessor admission; stop evidence before path-free root request; existing root record required for current-tag replay 00755911d37655eea3449e3fd69c884dd955a4f7d67487efa7cc6b9b9c7514f9
+internal/host/claimservingretire.go claimServingArtifactRetirement.recover authority C,J4 rev3=PROPOSED-seed; fresh paired tags and predecessor admission; stop evidence before path-free root request; existing root record required for current-tag replay; agent mount anchored before root call and same device/inode bound across namespaces; root records unchanged 7a501bc99cec3aca9591746cf254b2e16049f1ee154dde9d415e1649df468d85
internal/host/claimservingretire.go claimServingArtifactRetirement.snapshot authority C,J4 rev3=PROPOSED-seed; fresh claim and nested database path; paired inventory stamps, normalized paths and ancestor conflicts before root requests file:Read@908 202138f761733b9853154f6b9382e68d74e4e933a600949cdd284a9cf6df765a
internal/host/claimservingretire.go claimServingArtifactRetirement.stopped authority C,J4 rev3=PROPOSED-seed; completed serving fence receipts from the existing Guard backend; exact predecessor and byte-identical stop evidence ea358c5594a469b11d6255df9da2f0567f5d911fb94e1a3702f7cdc4f60c7751
internal/host/claimservingretire.go claimServingArtifactRetirement.validateAuthorization authority C,J4 rev3=PROPOSED-seed; root-returned retirement identity, original and continuing successor tuples, tag source stamp and source-directory identity 8324a81ca00f5d6cf79a38560b7a99f4de65d203545074b24bc2144fc26ac52d
@@ -325,12 +325,14 @@ system.go runSystem authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments,
system.go systemVerb authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, claim flags, service identity and root delegation capability before constructing agent-mode resources; one-shot skips Configure, Start, duties and listeners; close resources only after private runtime joins ee25513ed72f9b8338d95682c4c76b4ced26ec1f060528fc5804da9afbfbfd83
@coverage . bf3d70b6a2c5d769a3c580c9d5511231499b788d9c08a542895de8381e5a39c9
@coverage internal/activator 41f0ad01ac54bae63f089ef8ec5bbfa9155bb76720d72b2d88563481de40fe5c
-@coverage internal/host 276b5d95e109772c8af5bc7408bf4d888656ad032ef5d552e169203f55be023e
+@coverage internal/host 8872d86cde27a46eec09390c6bf7a35dfd977e4afca9825f2a32b068f420f038
@coverage internal/hostbox db890260533bdace6ad9a7c23dc1538cb919962894f005fc7c5a59a18e969429
@coverage internal/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
internal/host/rootexecution_recovery.go recoverRootExecutionBeforeUpdate authority D rev3=PROPOSED-seed; extracted update recovery ordering; root instance-control identity; broker durable controller state then best-effort local park and retirement journals; retained park failures cannot gate update eb97b162b0d712dd2fe8971984353ebc10efe1142077a3c2531781dd969cbbf5
internal/lease/leasetest/s3server.go NewS3Server authority C,J4 rev3=PROPOSED-seed; test-process S3 fixture only, including privileged runner; HTTP query selects simulated versions, GET exposes stored ETag and conditional PUT refuses stale owners under the fixture mutex; not called by production root paths file:Query@479 ac2435bd4d5114736ba9b7e964f62d5fac3dd97eee5f11cbadd4facc342a9583
+internal/host/claimservingartifact_linux.go servingAgentDirectory authority C,J4 rev3=PROPOSED-seed; agent descriptor identity before root RPC; no-follow directory open and existing statx device/inode/mount helper; used only for agent namespace checks 5edaa27cde0a470efb1a820ed15eca551df9c43fe6d7aa3c20f753bc3e3ae718
+internal/host/claimservingartifact_other.go servingAgentDirectory authority C,J4 rev3=PROPOSED-seed; non-Linux refusal; no directory read or permission ef4826226980538555ab8152b6f5a8167b599eec64a2e48e53a7d8bd5e8332e7
@coverage internal/lease/leasetest a589d673ded02391c4dfbbe6a9b86890f44522d3985bfa219c19d6ed40a0fd07
internal/instancectl/claimexec.go claimExecGate.authorize authority C,G,J2,J4 rev3=PROPOSED-seed; root instance-control; systemd MainPID refreshed only within the same active invocation and cgroup at accepted trampoline connection; exact kernel peer match and repeated Same checks before permit 2c7461c0ce1f033f163bb660214b9e906945e75a8067bb1d6b1faa3b6916649d
internal/instancectl/claimunit.go ClaimUnits.start authority C,G,J2,J4 rev3=PROPOSED-seed; root instance-control; retain the refreshed authenticated trampoline MainPID for post-start kernel launch verification and exact-invocation cleanup 7f01cf7ac8fd9db0f74f7b08c412ee7d229ff2e097ae1d69706d8b3c87f1e30d