carlos / platform Public

WIP files seed checkpoint 25 (not for merge)

Authority-review fixes FS-R1..R6 (wrapped-handler crash, recovery ordering and paging, per-journal isolation, pressure budget, census reclasses); Task 1 live-agent socket/client, killed-controller settlement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 22fb2d10b6462798a5e3ab0ab29fdb0065d13d3c parent 991ae5a
37 files changed, +1753 −130
  • internal/activator/cleanup.go +67 −0
  • internal/activator/cleanup_test.go +62 −0
  • internal/activator/servingretry.go +3 −3
  • internal/host/agent.go +43 −4
  • internal/host/claimfilesseed.go +18 −1
  • internal/host/claimfilesseed_command.go +11 −11
  • internal/host/claimfilesseed_delegation_linux.go +93 −0
  • internal/host/claimfilesseed_integration_linux_test.go +12 −1
  • internal/host/claimfilesseed_killed_linux.go +90 −0
  • internal/host/claimfilesseed_killed_linux_test.go +86 −0
  • internal/host/claimfilesseed_live_rootrunner_linux_test.go +153 −0
  • internal/host/claimfilesseed_restore_linux_test.go +5 −2
  • internal/host/claimfilesseed_runtime.go +1 −1
  • internal/host/claimfilesseed_runtime_test.go +35 −0
  • internal/host/claimfilesseed_transport_linux.go +301 −0
  • internal/host/claimfilesseed_transport_linux_test.go +244 −0
  • internal/host/claimfilesseed_transport_other.go +17 −0
  • internal/host/claimlaunch_production.go +2 −0
  • internal/host/claimrecovery_rulings_test.go +82 −0
  • internal/host/claimretire_transport.go +18 −27
  • internal/host/claimservingmaintenance.go +21 −2
  • internal/host/claimservingmaintenance_linux.go +3 −11
  • internal/host/claimservingpark_reap_test.go +32 −0
  • internal/host/claimservingpark_records.go +0 −26
  • internal/host/claimservingpark_recovery.go +15 −4
  • internal/host/claimservingpark_rootrunner_linux_test.go +18 −1
  • internal/host/claimservingpark_transport.go +4 −0
  • internal/host/claimservingpeer_rootrunner_linux_test.go +182 −2
  • internal/host/claimstop_kernel_linux.go +3 −0
  • internal/host/claimstop_run.go +5 −2
  • internal/host/rootexecution_recovery.go +8 −2
  • internal/hostbox/servinghandover.go +8 −4
  • internal/hostbox/servinghandover_linux_test.go +27 −0
  • internal/provision/plan14_legacy_identity_test.go +12 −3
  • internal/provision/plan14_root_reads_test.go +2 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +54 −23
  • system_test.go +16 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	304.111s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.735s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.287s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.705s
ok  	github.com/carlosframework/platform/internal/activator	249.810s
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.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.482s
ok  	github.com/carlosframework/platform/internal/apperr	0.074s
ok  	github.com/carlosframework/platform/internal/applog	0.128s
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.660s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.703s
ok  	github.com/carlosframework/platform/internal/changelog	0.304s
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	391.944s
?   	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.934s
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.365s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	26.780s
?   	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.134s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.010s
ok  	github.com/carlosframework/platform/internal/geoip	0.031s
ok  	github.com/carlosframework/platform/internal/host	163.293s
ok  	github.com/carlosframework/platform/internal/hostactors	0.367s
ok  	github.com/carlosframework/platform/internal/hostbox	2.470s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.520s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	4.939s
ok  	github.com/carlosframework/platform/internal/hostfence	0.030s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.514s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	1.024s
ok  	github.com/carlosframework/platform/internal/hostops	1.597s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.014s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.478s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.140s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.014s
ok  	github.com/carlosframework/platform/internal/infraview	0.014s
ok  	github.com/carlosframework/platform/internal/instancectl	4.163s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.727s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.531s
ok  	github.com/carlosframework/platform/internal/lease	9.817s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.019s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.165s
ok  	github.com/carlosframework/platform/internal/maintenance	0.090s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.562s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.081s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.197s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.780s
ok  	github.com/carlosframework/platform/internal/objstore	0.177s
ok  	github.com/carlosframework/platform/internal/ops	0.176s
ok  	github.com/carlosframework/platform/internal/park	0.210s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	16.566s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.991s
ok  	github.com/carlosframework/platform/internal/relay	7.239s
ok  	github.com/carlosframework/platform/internal/release	20.433s
ok  	github.com/carlosframework/platform/internal/rollout	3.767s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.008s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.575s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.489s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.825s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.083s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.244s
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.006s
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.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
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
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.024s
=== RUN   TestT4StorageProbeMinio
2026/10/15 16:06:54 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEBFA46B1277BE, 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/activator/cleanup.go b/internal/activator/cleanup.go
new file mode 100644
index 000000000..a472868cd
--- /dev/null
+++ b/internal/activator/cleanup.go
@@ -0,0 +1,67 @@
+package activator
+
+import (
+ "context"
+ "sync"
+ "time"
+)
+
+type cleanupBudgetKey struct{}
+type cleanupContextKey struct{}
+type cleanupBudget struct {
+ mu sync.Mutex
+ base context.Context
+ source context.Context
+ parent *cleanupBudget
+ limit time.Duration
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+func (b *cleanupBudget) start() context.Context {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ if b.ctx == nil {
+ base := b.base
+ if b.parent != nil && b.parent.source.Err() != nil {
+ base = b.parent.start()
+ }
+ b.ctx, b.cancel = context.WithTimeout(base, b.limit)
+ }
+ return b.ctx
+}
+
+// WithCleanupBudget shares the cancellation grace period across a request,
+// its nested failure paths, and the owner's final join. Normal work is untimed.
+func WithCleanupBudget(ctx context.Context, limit time.Duration) (context.Context, context.CancelFunc) {
+ parent, _ := ctx.Value(cleanupBudgetKey{}).(*cleanupBudget)
+ b := &cleanupBudget{base: context.WithoutCancel(ctx), source: ctx, parent: parent, limit: limit}
+ stop := context.AfterFunc(ctx, func() { b.start() })
+ return context.WithValue(ctx, cleanupBudgetKey{}, b), func() { stop(); b.start(); b.cancel() }
+}
+
+// CleanupContext keeps an existing cleanup deadline. Cancellation starts the
+// owner's single grace period; it never grants each nested call a fresh one.
+func CleanupContext(ctx context.Context, limit time.Duration) (context.Context, context.CancelFunc) {
+ if ctx.Value(cleanupContextKey{}) != nil {
+ return context.WithTimeout(ctx, limit)
+ }
+ base := context.WithoutCancel(ctx)
+ var stop func() bool
+ if b, ok := ctx.Value(cleanupBudgetKey{}).(*cleanupBudget); ok {
+ if ctx.Err() != nil {
+ base = b.start()
+ }
+ cleanup, cancel := context.WithTimeout(base, limit)
+ stop = context.AfterFunc(ctx, func() { budget := b.start(); context.AfterFunc(budget, cancel) })
+ return context.WithValue(cleanup, cleanupContextKey{}, true), func() { stop(); cancel() }
+ }
+ // Callers without an owner budget still cannot extend an explicit deadline.
+ if deadline, ok := ctx.Deadline(); ok {
+ bounded, cancel := context.WithDeadline(base, deadline)
+ cleanup, done := context.WithTimeout(bounded, limit)
+ return context.WithValue(cleanup, cleanupContextKey{}, true), func() { done(); cancel() }
+ }
+ cleanup, cancel := context.WithTimeout(base, limit)
+ return context.WithValue(cleanup, cleanupContextKey{}, true), cancel
+}
diff --git a/internal/activator/cleanup_test.go b/internal/activator/cleanup_test.go
new file mode 100644
index 000000000..06e30357c
--- /dev/null
+++ b/internal/activator/cleanup_test.go
@@ -0,0 +1,62 @@
+package activator
+
+import (
+ "context"
+ "testing"
+ "time"
+)
+
+func TestCleanupBudgetSharedAfterCancellation(t *testing.T) {
+ parent, stop := context.WithCancel(t.Context())
+ ctx, finish := WithCleanupBudget(parent, 80*time.Millisecond)
+ defer finish()
+ stop()
+ first, cancel := CleanupContext(ctx, time.Second)
+ defer cancel()
+ deadline, ok := first.Deadline()
+ if !ok || time.Until(deadline) > 100*time.Millisecond {
+ t.Fatal("cleanup escaped shutdown budget")
+ }
+ nested, done := CleanupContext(first, time.Hour)
+ defer done()
+ if next, _ := nested.Deadline(); next.After(deadline) {
+ t.Fatal("nested cleanup extended deadline")
+ }
+ <-first.Done()
+ later, end := CleanupContext(ctx, time.Second)
+ defer end()
+ if later.Err() == nil {
+ t.Fatal("cleanup budget restarted")
+ }
+}
+
+func TestCleanupBudgetDoesNotLimitNormalOperation(t *testing.T) {
+ ctx, finish := WithCleanupBudget(t.Context(), time.Millisecond)
+ defer finish()
+ time.Sleep(3 * time.Millisecond)
+ cleanup, cancel := CleanupContext(ctx, time.Second)
+ defer cancel()
+ if cleanup.Err() != nil || ctx.Err() != nil {
+ t.Fatal("normal request exhausted shutdown budget")
+ }
+}
+
+func TestCleanupBudgetNestedOwnerUsesShutdownDeadline(t *testing.T) {
+ parent, stop := context.WithCancel(t.Context())
+ parent, finish := WithCleanupBudget(parent, 60*time.Millisecond)
+ defer finish()
+ request, cancel := context.WithCancel(parent)
+ defer cancel()
+ request, end := WithCleanupBudget(request, time.Second)
+ defer end()
+ stop()
+ owner, done := CleanupContext(parent, time.Second)
+ defer done()
+ handler, joined := CleanupContext(request, time.Second)
+ defer joined()
+ a, _ := owner.Deadline()
+ b, _ := handler.Deadline()
+ if !a.Equal(b) {
+ t.Fatalf("handler extended shared deadline: %v %v", a, b)
+ }
+}
diff --git a/internal/activator/servingretry.go b/internal/activator/servingretry.go
index 7e4aa3dd1..d0ab0c193 100644
--- a/internal/activator/servingretry.go
+++ b/internal/activator/servingretry.go
@@ -97,7 +97,7 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
cleanupObservation := o
defer func() {
if resultErr != nil {
- recovery, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ recovery, cancel := CleanupContext(ctx, 10*time.Second)
defer cancel()
resultErr = errors.Join(resultErr, base.FailedAdmission(recovery, cleanupObservation, resultErr))
}
@@ -133,7 +133,7 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
err = hostbox.ErrChanged
}
if err != nil {
- recovery, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ recovery, cancel := CleanupContext(ctx, 10*time.Second)
defer cancel()
// An ambiguous admission or bind is settled using the stored attempt, never refunded.
fresh, readErr := hostclaim.Read(recovery, d.Claims, o.Host)
@@ -212,7 +212,7 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) {
return err
}
s.Failed = func(ctx context.Context, _ int) error {
- recovery, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
+ recovery, cancel := CleanupContext(ctx, 10*time.Second)
defer cancel()
fresh, err := hostclaim.Read(recovery, d.Claims, o.Host)
if err != nil {
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 448379b50..7caebda1d 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -159,6 +159,7 @@ import (
"net/http"
"os"
"os/exec"
+ "os/signal"
"path"
"path/filepath"
"runtime"
@@ -166,6 +167,7 @@ import (
"strings"
"sync"
"sync/atomic"
+ "syscall"
"time"
"github.com/carlosframework/platform/internal/activator"
@@ -664,9 +666,22 @@ func runAgent(args []string, buildClaims ClaimFactory) {
close(started)
}
+ safeClaimClose := true
+ var shutdownCtx context.Context
+ var shutdownCancel context.CancelFunc
defer func() {
+ if shutdownCancel != nil {
+ defer shutdownCancel()
+ }
cancelAgent()
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ if !safeClaimClose {
+ return
+ }
+ base := shutdownCtx
+ if base == nil {
+ base = context.Background()
+ }
+ ctx, cancel := context.WithTimeout(base, 5*time.Second)
defer cancel()
if err := claims.close(ctx); err != nil {
log.Printf("agent: regional claims shutdown: %v", err)
@@ -813,6 +828,21 @@ func runAgent(args []string, buildClaims ClaimFactory) {
go runDuties()
}
+ // Seed handlers borrow this agent's resources and finish before they close.
+ seedCtx, seedCancel := signal.NotifyContext(claimCtx, syscall.SIGTERM, os.Interrupt)
+ defer seedCancel()
+ seedCtx, seedFinish := activator.WithCleanupBudget(seedCtx, 30*time.Second)
+ defer seedFinish()
+ stopSeed := context.AfterFunc(startupCtx, seedCancel)
+ defer stopSeed()
+ var seedServer *filesSeedServer
+ if claims.Writes.Enabled && os.Getenv("CARLOS_HOST_CLAIM_ORIGINS") == "1" && svc != nil {
+ seedServer, err = startAgentFilesSeed(seedCtx, svc.Startup, svc)
+ if err != nil {
+ log.Printf("Files seed failed: %s.", err)
+ }
+ }
+
// The same serving loop as `carlos edge`, with the activator injected.
// Caching applies here too: cached hits still avoid the activator, which
// is the entire point of checking the cache before the hibernation branch.
@@ -884,8 +914,18 @@ func runAgent(args []string, buildClaims ClaimFactory) {
go usageLoop(m.Drain, am)
}, edgeOpts...)
+ shutdownCtx, shutdownCancel = activator.CleanupContext(context.Background(), 60*time.Second)
cancelStartup()
- joinCtx, joinCancel := context.WithTimeout(context.Background(), 5*time.Second)
+ seedCancel()
+ seedJoin, seedJoinCancel := activator.CleanupContext(seedCtx, 30*time.Second)
+ seedErr := seedServer.close(seedJoin)
+ seedJoinCancel()
+ if seedErr != nil {
+ safeClaimClose = false
+ log.Printf("Files seed failed: %s.", seedErr)
+ os.Exit(1)
+ }
+ joinCtx, joinCancel := context.WithTimeout(shutdownCtx, 5*time.Second)
defer joinCancel()
joinAgentStartup(joinCtx, startupDone)
joinAgentStartup(joinCtx, dutiesDone)
@@ -893,8 +933,7 @@ func runAgent(args []string, buildClaims ClaimFactory) {
// The edge has drained; take the children down too (they're this process's
// children — orphaning them would strand their leases until TTL expiry).
if svc != nil {
- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
- defer cancel()
+ ctx := shutdownCtx
svc.Shutdown(ctx)
// Then the scheduler, under the same budget (spec §8.6, Task 11): an
// attempt whose swap Shutdown just cancelled finishes its done(), its
diff --git a/internal/host/claimfilesseed.go b/internal/host/claimfilesseed.go
index 9847e3e41..880e912ba 100644
--- a/internal/host/claimfilesseed.go
+++ b/internal/host/claimfilesseed.go
@@ -215,7 +215,7 @@ func (e *filesSeedExecutor) host(ctx context.Context, o hostclaim.Observed, out
return err
}
defer func() {
- reportCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), hostbox.RequestLimit)
+ reportCtx, cancel := activator.CleanupContext(ctx, hostbox.RequestLimit)
defer cancel()
fresh, err := hostclaim.Read(reportCtx, a.Claims, o.Host)
if err == nil {
@@ -264,3 +264,20 @@ func (e *filesSeedExecutor) host(ctx context.Context, o hostclaim.Observed, out
_, err = fmt.Fprintf(out, "Host %q: seeded.\n", o.Host)
return err
}
+
+// close is called only after the serial handler has joined. A failed private
+// runtime stays retained until it joins; shared owners must not close beneath it.
+func (e *filesSeedExecutor) close(ctx context.Context) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ if e.running {
+ return activator.ErrFenceHeld
+ }
+ var result error
+ for _, pending := range e.pending {
+ if pending.wake != nil {
+ result = errors.Join(result, pending.wake.Close(ctx))
+ }
+ }
+ return result
+}
diff --git a/internal/host/claimfilesseed_command.go b/internal/host/claimfilesseed_command.go
index fa1b4112f..f67725c76 100644
--- a/internal/host/claimfilesseed_command.go
+++ b/internal/host/claimfilesseed_command.go
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"flag"
- "fmt"
"io"
"log"
"os"
@@ -48,8 +47,10 @@ func parseFilesSeed(args []string) (string, error) {
}
func RunSystemFiles(args []string) {
- ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
+ signalCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer cancel()
+ ctx, finish := activator.WithCleanupBudget(signalCtx, 30*time.Second)
+ defer finish()
if err := runSystemFiles(ctx, args, os.Stdout); err != nil {
log.Printf("Files seed failed: %s.", err)
os.Exit(filesSeedExitCode(err))
@@ -84,9 +85,12 @@ func runSystemFiles(ctx context.Context, args []string, out io.Writer) error {
return errFilesSeedDelegation
}
pid, err := filesSeedEdgePID(ctx)
- if err != nil || pid != os.Getpid() {
+ if err != nil {
return errFilesSeedDelegation
}
+ if pid != os.Getpid() {
+ return filesSeedClient(ctx, host, out, uid)
+ }
manager, err := filesSeedManager(ctx)
if err != nil {
return err
@@ -135,16 +139,12 @@ func runFilesSeedOneShot(ctx context.Context, host string, out io.Writer, manage
}
var executor *filesSeedExecutor
defer func() {
- cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
+ cleanup, cancel := activator.CleanupContext(ctx, 30*time.Second)
defer cancel()
if executor != nil {
- for name, pending := range executor.pending {
- if pending.wake != nil {
- if err := pending.wake.Close(cleanup); err != nil {
- safeToClose = false
- resultErr = errors.Join(resultErr, fmt.Errorf("%s: %w", name, err))
- }
- }
+ if err := executor.close(cleanup); err != nil {
+ safeToClose = false
+ resultErr = errors.Join(resultErr, err)
}
}
if safeToClose {
diff --git a/internal/host/claimfilesseed_delegation_linux.go b/internal/host/claimfilesseed_delegation_linux.go
index a347220c0..c9d540795 100644
--- a/internal/host/claimfilesseed_delegation_linux.go
+++ b/internal/host/claimfilesseed_delegation_linux.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/carlosframework/platform/internal/cgroup"
"golang.org/x/sys/unix"
+ "net"
"os"
"os/exec"
"path/filepath"
@@ -130,3 +131,95 @@ func filesSeedManager(ctx context.Context) (*cgroup.Manager, error) {
}
return cgroup.SetupAtRetaining(root, func(string, ...any) {})
}
+
+// The connection supplies the PID. pidfd pins that process while two complete
+// observations detect exit, reuse, cgroup movement and an edge restart.
+func authenticateFilesSeedPeer(ctx context.Context, conn *net.UnixConn, uid int, server bool) error {
+ raw, err := conn.SyscallConn()
+ if err != nil {
+ return err
+ }
+ var peer *unix.Ucred
+ var peerErr error
+ if err := raw.Control(func(fd uintptr) { peer, peerErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil {
+ return err
+ }
+ if peerErr != nil {
+ return peerErr
+ }
+ if peer == nil || peer.Pid <= 1 || peer.Uid != uint32(uid) || uid == 0 {
+ return ErrClaimStopIdentity
+ }
+ fd, err := unix.PidfdOpen(int(peer.Pid), 0)
+ if err != nil {
+ return err
+ }
+ defer unix.Close(fd)
+ first, err := filesSeedPeerObservation(ctx, int(peer.Pid), server)
+ if err != nil {
+ return err
+ }
+ second, err := filesSeedPeerObservation(ctx, int(peer.Pid), server)
+ if err != nil {
+ return err
+ }
+ if first != second {
+ return ErrClaimStopIdentity
+ }
+ if err := unix.PidfdSendSignal(fd, 0, nil, 0); err != nil {
+ return err
+ }
+ return ctx.Err()
+}
+
+func filesSeedPeerObservation(ctx context.Context, pid int, server bool) (string, error) {
+ edge, err := filesSeedEdgePID(ctx)
+ if err != nil {
+ return "", err
+ }
+ if server && edge != pid || !server && edge != os.Getpid() {
+ return "", ErrClaimStopIdentity
+ }
+ controller, err := (productionStopControllers{}).VerifiedController(ctx, edge)
+ if err != nil {
+ return "", err
+ }
+ root, err := agentUnitCgroup(ctx)
+ if err != nil {
+ return "", err
+ }
+ rel, err := filepath.Rel("/sys/fs/cgroup", root)
+ if err != nil || !filepath.IsLocal(rel) || rel == "." {
+ return "", ErrClaimStopIdentity
+ }
+ raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
+ if err != nil {
+ return "", err
+ }
+ end := strings.LastIndexByte(string(raw), ')')
+ if end < 0 {
+ return "", ErrClaimStopIdentity
+ }
+ fields := strings.Fields(string(raw)[end+1:])
+ if len(fields) < 20 || fields[0] == "Z" {
+ return "", ErrClaimStopIdentity
+ }
+ groups, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
+ if err != nil {
+ return "", err
+ }
+ group := strings.TrimSpace(string(groups))
+ if !strings.HasPrefix(group, "0::/") || strings.Contains(group, "\n") {
+ return "", ErrClaimStopIdentity
+ }
+ member := strings.TrimPrefix(group, "0::")
+ unit := "/" + filepath.ToSlash(rel)
+ if server {
+ if member != unit+"/supervise" {
+ return "", ErrClaimStopIdentity
+ }
+ } else if member == unit || strings.HasPrefix(member, unit+"/") {
+ return "", ErrClaimStopIdentity
+ }
+ return fmt.Sprintf("%d:%s:%s:%s:%s", edge, controller, root, fields[19], group), nil
+}
diff --git a/internal/host/claimfilesseed_integration_linux_test.go b/internal/host/claimfilesseed_integration_linux_test.go
index 7669e6b12..aad01e295 100644
--- a/internal/host/claimfilesseed_integration_linux_test.go
+++ b/internal/host/claimfilesseed_integration_linux_test.go
@@ -20,7 +20,7 @@ import (
"github.com/carlosframework/platform/internal/release"
)
-func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation) {
+func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation, live ...filesSeedLiveWitness) {
t.Helper()
f := p.f
f.actor.Bucket = f.f.bucket
@@ -127,6 +127,9 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation) {
normal := newServingPreparation(t, f, "boot", "controller", func(w *ClaimWakeAssembly) {
w.Inputs.(*claimServingOwners).Sources.Files = ordinary
w.ServingPrerequisites = servingFilesPrerequisites(f.f.claims, ordinary)
+ for _, witness := range live {
+ witness.configure(w)
+ }
})
admitted, start := normal.prepare(t)
if err := start.Restore(t.Context()); err != nil {
@@ -150,6 +153,9 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation) {
if err != nil || string(b) != "real seeded bytes" {
t.Fatal("restored files", string(b), err)
}
+ for _, witness := range live {
+ witness.serve(t, normal, admitted, start)
+ }
if err := start.Failed(t.Context(), 0); err != nil {
t.Fatal(err)
}
@@ -166,6 +172,11 @@ func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation) {
t.Log(output.String())
}
+type filesSeedLiveWitness struct {
+ configure func(*ClaimWakeAssembly)
+ serve func(*testing.T, *servingPreparation, hostclaim.Observed, activator.ClaimStart)
+}
+
func TestFilesSeedRealOriginPreparation(t *testing.T) {
runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller")
}
diff --git a/internal/host/claimfilesseed_killed_linux.go b/internal/host/claimfilesseed_killed_linux.go
new file mode 100644
index 000000000..d2a1e4915
--- /dev/null
+++ b/internal/host/claimfilesseed_killed_linux.go
@@ -0,0 +1,90 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+// systemd removes an edge unit's empty delegated subtree after SIGKILL. A
+// seed that never reserved a tenant role can settle that absence in the same
+// boot. This path neither kills a replacement group nor invents a reboot.
+func (k LinuxStopKernel) recoverUnlaunchedAbsent(ctx context.Context, t hostguard.FenceTarget, in StopInventory, j hostbox.StopKernelJournal, journal interface {
+ SettleRecovered(context.Context, hostguard.FenceTarget) error
+}) (StopKernelEvidence, DrainAcknowledgement, error) {
+ refuse := func(err error) (StopKernelEvidence, DrainAcknowledgement, error) {
+ return StopKernelEvidence{}, DrainAcknowledgement{}, err
+ }
+ if j.V != 1 || j.PendingRoles || len(j.Cgroups) != 1 || len(j.UnitReservationIDs) != 0 {
+ return refuse(ErrClaimStopInventory)
+ }
+ raw, ok := k.Journal.(interface {
+ Read(context.Context, hostguard.FenceTarget) (hostbox.LaunchInventory, error)
+ })
+ if !ok {
+ return refuse(ErrClaimStopInventory)
+ }
+ launch, err := raw.Read(ctx, t)
+ if err != nil {
+ return refuse(err)
+ }
+ if len(launch.Reservations) != 0 || len(launch.Groups) != 1 {
+ return refuse(ErrClaimStopInventory)
+ }
+ paths, err := k.stopDescriptorPaths(in)
+ if err != nil {
+ return refuse(err)
+ }
+ check := func(expected hostbox.StopKernelJournal) error {
+ fresh, err := k.readContainedJournal(ctx, t)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(fresh, expected) {
+ return ErrClaimStopInventory
+ }
+ again, err := raw.Read(ctx, t)
+ if err != nil {
+ return err
+ }
+ if len(again.Reservations) != 0 || !reflect.DeepEqual(again.Groups, launch.Groups) {
+ return ErrClaimStopInventory
+ }
+ boot, err := recoveryControllerAt(k.ProcRoot, t, fresh)
+ if err != nil {
+ return err
+ }
+ if boot != t.BootID {
+ return ErrClaimStopIdentity
+ }
+ if _, err := os.Lstat(filepath.Join(k.CgroupRoot, j.Cgroups[0].RelativePath)); !errors.Is(err, os.ErrNotExist) {
+ if err != nil {
+ return err
+ }
+ return ErrClaimStopInventory
+ }
+ return scanStopDescriptors(ctx, k.ProcRoot, j.Files, paths)
+ }
+ if err := check(j); err != nil {
+ return refuse(err)
+ }
+ if err := journal.SettleRecovered(ctx, t); err != nil {
+ return refuse(err)
+ }
+ settled := j
+ settled.Settled = true
+ if err := check(settled); err != nil {
+ return refuse(err)
+ }
+ group := j.Cgroups[0]
+ evidence := StopKernelEvidence{ObservedBootID: t.BootID, InventorySHA256: digestStop(in), LocalJournalSHA256: digestStop(settled), CgroupIdentities: []string{fmt.Sprintf("unlaunched-absent=%s;previous_identity=%d:%d;controller=%s", group.RelativePath, group.Device, group.Inode, t.ControllerID)}, UnitInvocationIDs: []string{}}
+ return evidence, DrainClaimAcknowledgement(t.FenceID, t.ControllerID, j.ProcessStart, j.ProducerIDs, j.Completed), nil
+}
diff --git a/internal/host/claimfilesseed_killed_linux_test.go b/internal/host/claimfilesseed_killed_linux_test.go
new file mode 100644
index 000000000..b3b62b8f0
--- /dev/null
+++ b/internal/host/claimfilesseed_killed_linux_test.go
@@ -0,0 +1,86 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+type seedAbsentJournal struct {
+ rebootJournalFixture
+ raw hostbox.LaunchInventory
+ after func()
+}
+
+func (j *seedAbsentJournal) Read(context.Context, hostguard.FenceTarget) (hostbox.LaunchInventory, error) {
+ return j.raw, nil
+}
+func (j *seedAbsentJournal) SettleRecovered(context.Context, hostguard.FenceTarget) error {
+ j.j.Settled = true
+ if j.after != nil {
+ j.after()
+ }
+ return nil
+}
+
+func TestFilesSeedKilledUnlaunchedRecovery(t *testing.T) {
+ for _, fault := range []string{"none", "live-controller", "reused-parent", "reappears", "role", "boot-change", "descriptor", "foreign-parent", "no-journal"} {
+ t.Run(fault, func(t *testing.T) {
+ root, proc := t.TempDir(), t.TempDir()
+ os.MkdirAll(filepath.Join(proc, "sys/kernel/random"), 0700)
+ os.WriteFile(filepath.Join(proc, "sys/kernel/random/boot_id"), []byte("boot\n"), 0600)
+ target := hostguard.FenceTarget{Host: "seed.example", Incarnation: "inc", Stamp: 11, Session: 13, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller", FenceID: "fence"}
+ agent := "/sys/fs/cgroup/system.slice/carlos-edge.service"
+ parent, err := claimContainmentParent(agent, target)
+ if err != nil {
+ t.Fatal(err)
+ }
+ group := hostbox.StopCgroupIdentity{RelativePath: parent, Device: 1, Inode: 2}
+ j := &seedAbsentJournal{rebootJournalFixture: rebootJournalFixture{j: hostbox.StopKernelJournal{V: 1, Target: target, ControllerPID: 42, ProcessStart: "123", Cgroups: []hostbox.StopCgroupIdentity{group}}}, raw: hostbox.LaunchInventory{Target: target, Groups: []hostbox.LaunchGroup{{RelativePath: parent, Device: 1, Inode: 2}}, NoState: true}}
+ state := t.TempDir()
+ k := LinuxStopKernel{AgentCgroup: func(context.Context) (string, error) { return agent, nil }, Journal: j, CgroupRoot: root, ProcRoot: proc, ManagedRoots: map[string]string{"data": state}}
+ switch fault {
+ case "live-controller":
+ os.MkdirAll(filepath.Join(proc, "42"), 0700)
+ os.WriteFile(filepath.Join(proc, "42/stat"), []byte("42 (fixture) S "+strings.Repeat("0 ", 18)+"123 0"), 0600)
+ case "reused-parent":
+ os.MkdirAll(filepath.Join(root, parent), 0700)
+ case "reappears":
+ j.after = func() { os.MkdirAll(filepath.Join(root, parent), 0700) }
+ case "role":
+ j.j.UnitReservationIDs = []string{"main"}
+ case "boot-change":
+ os.WriteFile(filepath.Join(proc, "sys/kernel/random/boot_id"), []byte("other"), 0600)
+ case "descriptor":
+ os.MkdirAll(filepath.Join(proc, "42/fd"), 0700)
+ os.WriteFile(filepath.Join(proc, "42/stat"), []byte("42 (fixture) S "+strings.Repeat("0 ", 18)+"456 0"), 0600)
+ os.Symlink(filepath.Join(state, "data.db")+" (deleted)", filepath.Join(proc, "42/fd/3"))
+ case "foreign-parent":
+ j.j.Cgroups[0].RelativePath = "other/group"
+ case "no-journal":
+ k.Journal = &j.rebootJournalFixture
+ }
+ in := StopInventory{Complete: true, Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "data.db"}}}
+ evidence, _, err := k.recoverUnlaunchedAbsent(t.Context(), target, in, j.j, j)
+ if fault == "none" {
+ if err != nil || !j.j.Settled || evidence.ObservedBootID != "boot" {
+ t.Fatal(err, evidence)
+ }
+ return
+ }
+ if err == nil {
+ t.Fatal("unsafe recovery accepted", fault)
+ }
+ if fault != "reappears" && j.j.Settled {
+ t.Fatal("refusal settled journal")
+ }
+ })
+ }
+}
diff --git a/internal/host/claimfilesseed_live_rootrunner_linux_test.go b/internal/host/claimfilesseed_live_rootrunner_linux_test.go
new file mode 100644
index 000000000..0e69299b7
--- /dev/null
+++ b/internal/host/claimfilesseed_live_rootrunner_linux_test.go
@@ -0,0 +1,153 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/hex"
+ "flag"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+var seedTenantSocket = flag.String("socket", "", "")
+var seedTenantDB = flag.String("db", "", "")
+
+func init() {
+ if os.Getenv("FILES_SEED_TENANT") == "1" {
+ os.Args = append(os.Args, "-test.run=^TestRootFilesSeedTenantPayload$", "-test.timeout=30s")
+ }
+}
+
+// This process is launched from the ordinary selected Build closure after C8.
+// The surrounding fixture still supplies role authority and stop evidence;
+// this witness does not stand in for the installed isolated role-launch test.
+func TestRootFilesSeedTenantPayload(t *testing.T) {
+ if os.Getenv("FILES_SEED_TENANT") != "1" {
+ t.Fatal("invoke through real-origin tenant witness")
+ }
+ db, err := sql.Open("sqlite", *seedTenantDB)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ var integrity string
+ if err := db.QueryRow("PRAGMA integrity_check").Scan(&integrity); err != nil || integrity != "ok" {
+ t.Fatal(integrity, err)
+ }
+ body, err := os.ReadFile(os.Getenv("FILES_SEED_PAYLOAD"))
+ if err != nil || string(body) != "real seeded bytes" {
+ t.Fatal(string(body), err)
+ }
+ listener, err := net.Listen("unix", *seedTenantSocket)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer listener.Close()
+ server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(body) })}
+ defer server.Close()
+ if err := server.Serve(listener); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func rootFilesSeedLiveWitness(t *testing.T, binary string) filesSeedLiveWitness {
+ body, err := os.ReadFile(binary)
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest := sha256.Sum256(body)
+ return filesSeedLiveWitness{configure: func(w *ClaimWakeAssembly) {
+ owners := w.Inputs.(*claimServingOwners)
+ selectLaunch := owners.Sources.Launch
+ owners.Sources.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
+ launch, err := selectLaunch(ctx, rt)
+ if err != nil {
+ return launch, err
+ }
+ launch.Executable = binary
+ launch.ExecutableSHA256 = hex.EncodeToString(digest[:])
+ launch.Env = append(launch.Env, "FILES_SEED_TENANT=1", "FILES_SEED_PAYLOAD="+filepath.Join(filepath.Dir(rt.DBPath), rt.Host, "payload"))
+ return launch, nil
+ }
+ }, serve: func(t *testing.T, p *servingPreparation, o hostclaim.Observed, start activator.ClaimStart) {
+ socket := provision.NestedSocket(o.Host)
+ if err := os.MkdirAll(filepath.Dir(socket), 0700); err != nil {
+ t.Fatal(err)
+ }
+ cmd := start.Build()
+ cmd.Stdin = os.Stdin
+ var output bytes.Buffer
+ cmd.Stdout = &output
+ cmd.Stderr = &output
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ // No readiness or success is fabricated: the selected process owns the socket.
+ defer func() { cmd.Process.Kill(); cmd.Wait(); os.Remove(socket) }()
+ ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
+ defer cancel()
+ if err := start.Ready(ctx, cmd); err != nil {
+ cmd.Process.Kill()
+ cmd.Wait()
+ t.Fatalf("tenant readiness: %v %s", err, output.String())
+ }
+ transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, "unix", socket)
+ }}
+ defer transport.CloseIdleConnections()
+ response, err := (&http.Client{Transport: transport, Timeout: time.Second}).Get("http://tenant/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ payload, err := io.ReadAll(response.Body)
+ response.Body.Close()
+ if err != nil || response.StatusCode != 200 || string(payload) != "real seeded bytes" {
+ t.Fatal("tenant served wrong restored payload", err, string(payload))
+ }
+ wrong, _ := os.FindProcess(os.Getpid())
+ refuse, stop := context.WithTimeout(t.Context(), 40*time.Millisecond)
+ defer stop()
+ if err := start.Ready(refuse, &exec.Cmd{Process: wrong}); err == nil {
+ t.Fatal("readiness accepted controller instead of tenant")
+ }
+ }}
+}
+
+func TestFilesSeedLiveFixtureShape(t *testing.T) {
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ witness := rootFilesSeedLiveWitness(t, binary)
+ f := plan9D5Fixture(t)
+ rt := registry.Route{Host: recoveryHost, DBPath: provision.NestedDBPath("/data", recoveryHost), Socket: provision.NestedSocket(recoveryHost)}
+ f.launch.Args = []string{"--socket", rt.Socket, "--db", rt.DBPath}
+ f.launch.Env = []string{"CARLOS_ADMIN_TOKEN=test"}
+ w := &ClaimWakeAssembly{Inputs: f.owners}
+ witness.configure(w)
+ launch, err := f.owners.Sources.Launch(t.Context(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ role := &ClaimRoleAssembly{Root: fakeLaunchRoot{}, ManagedRoots: map[string]string{"data": "/data"}}
+ in := ClaimLaunchInputs{Limit: cgroup.TierDefault, Roles: []string{"main"}, Identity: launch.Identity, Executable: launch.Executable, ExecutableSHA256: launch.ExecutableSHA256, Args: launch.Args, Env: launch.Env, DBPath: rt.DBPath}
+ if err := role.PreflightLaunch(rt.Host, in); err != nil {
+ t.Fatal(err, launch.Args, launch.Env)
+ }
+}
diff --git a/internal/host/claimfilesseed_restore_linux_test.go b/internal/host/claimfilesseed_restore_linux_test.go
index 080fb483a..15bd3a35b 100644
--- a/internal/host/claimfilesseed_restore_linux_test.go
+++ b/internal/host/claimfilesseed_restore_linux_test.go
@@ -151,7 +151,7 @@ func newServingPreparation(t *testing.T, f *servingRootFixture, boot, controller
func (p *servingPreparation) prepare(t *testing.T) (hostclaim.Observed, activator.ClaimStart) {
t.Helper()
f := p.f
- rt := registry.Route{Host: recoveryHost, DBPath: f.db}
+ rt := registry.Route{Host: recoveryHost, DBPath: f.db, Socket: provision.NestedSocket(recoveryHost)}
o := f.f.read()
// Prerequisites' isolated launch preflight requires an installed /data
// namespace. Drive its selection, stop and real lease in the same order;
@@ -216,6 +216,9 @@ func TestFilesSeedOrdinaryPreparationRestore(t *testing.T) {
}
func runFilesSeedOrdinaryPreparation(t *testing.T, failure, restart string, mounted ...bool) {
+ runFilesSeedPreparationWithLive(t, failure, restart, mounted, nil)
+}
+func runFilesSeedPreparationWithLive(t *testing.T, failure, restart string, mounted []bool, live []filesSeedLiveWitness) {
if failure == "real-seed" {
t.Setenv("CARLOS_HOST_CLAIM", "1")
t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
@@ -409,7 +412,7 @@ func runFilesSeedOrdinaryPreparation(t *testing.T, failure, restart string, moun
}
p := newServingPreparation(t, f, "boot", "controller")
if failure == "real-seed" {
- runFilesSeedRealPreparation(t, p)
+ runFilesSeedRealPreparation(t, p, live...)
return
}
var legacy servingParkRecord
diff --git a/internal/host/claimfilesseed_runtime.go b/internal/host/claimfilesseed_runtime.go
index e242f2755..c12639f6e 100644
--- a/internal/host/claimfilesseed_runtime.go
+++ b/internal/host/claimfilesseed_runtime.go
@@ -67,7 +67,7 @@ func prepareFilesSeed(ctx context.Context, rt registry.Route, runtime *activator
cleanupObservation := o
var start activator.ClaimStart
defer func() {
- cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
+ cleanup, cancel := activator.CleanupContext(ctx, 30*time.Second)
defer cancel()
var stopErr error
if start.Failed != nil {
diff --git a/internal/host/claimfilesseed_runtime_test.go b/internal/host/claimfilesseed_runtime_test.go
index eaef16a78..c4a4bd2ae 100644
--- a/internal/host/claimfilesseed_runtime_test.go
+++ b/internal/host/claimfilesseed_runtime_test.go
@@ -6,6 +6,7 @@ import (
"os/exec"
"reflect"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/hostbox"
@@ -153,3 +154,37 @@ func TestFilesSeedExitCodes(t *testing.T) {
t.Fatal("incorrect status")
}
}
+
+func TestFilesSeedPreparationCleanupSharesCanceledBudget(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ ctx, finish := activator.WithCleanupBudget(ctx, 50*time.Millisecond)
+ defer finish()
+ obs := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}}
+ token, err := (&activator.Service{}).AcquireSeedExclusion(ctx, recoveryHost, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var deadline time.Time
+ runtime := &activator.ClaimRuntime{
+ Read: func(ctx context.Context, _ string) (hostclaim.Observed, error) { return obs, ctx.Err() },
+ Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error { return nil },
+ Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) {
+ return obs, nil
+ },
+ Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) {
+ return activator.ClaimStart{Restore: func(context.Context) error { return nil }, Reserve: func(context.Context, int) error { return nil }, Materialize: func(context.Context) error { cancel(); return context.Canceled }, Failed: func(ctx context.Context, _ int) error { deadline, _ = ctx.Deadline(); <-ctx.Done(); return ctx.Err() }}, nil
+ },
+ FailedAdmission: func(ctx context.Context, _ hostclaim.Observed, _ error) error {
+ d, _ := ctx.Deadline()
+ if !d.Equal(deadline) || ctx.Err() == nil {
+ t.Error("cleanup received fresh budget")
+ }
+ return ctx.Err()
+ },
+ }
+ started := time.Now()
+ _, settled, err := prepareFilesSeed(ctx, registry.Route{Host: recoveryHost}, runtime, obs, token)
+ if err == nil || settled || time.Since(started) > time.Second || token.Check(recoveryHost) != nil {
+ t.Fatal("unbounded or falsely settled cleanup", err, settled)
+ }
+}
diff --git a/internal/host/claimfilesseed_transport_linux.go b/internal/host/claimfilesseed_transport_linux.go
new file mode 100644
index 000000000..049d2559c
--- /dev/null
+++ b/internal/host/claimfilesseed_transport_linux.go
@@ -0,0 +1,301 @@
+//go:build linux
+
+package host
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "golang.org/x/sys/unix"
+)
+
+const filesSeedSocket = "/run/carlos/files-seed.sock"
+
+type filesSeedRequest struct {
+ Verb string `json:"verb"`
+ Host string `json:"host,omitempty"`
+}
+type filesSeedReply struct {
+ Text string `json:"text,omitempty"`
+ Code *int `json:"code,omitempty"`
+}
+type filesSeedServer struct {
+ listener *net.UnixListener
+ cancel context.CancelFunc
+ done chan struct{}
+ handlers sync.WaitGroup
+ finalize func(context.Context) error
+}
+type filesSeedStream struct {
+ conn net.Conn
+ encoder *json.Encoder
+}
+
+func (s filesSeedStream) Write(p []byte) (int, error) {
+ s.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
+ if err := s.encoder.Encode(filesSeedReply{Text: string(p)}); err != nil {
+ return 0, err
+ }
+ return len(p), nil
+}
+
+func startFilesSeedSocket(ctx context.Context, ready <-chan struct{}, path string, auth func(context.Context, *net.UnixConn) error, run func(context.Context, string, io.Writer) error) (*filesSeedServer, error) {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-ready:
+ }
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ if auth == nil || run == nil {
+ return nil, hostbox.ErrUnsupported
+ }
+ parent, err := os.Lstat(filepath.Dir(path))
+ if err != nil {
+ return nil, err
+ }
+ var dir unix.Stat_t
+ if err := unix.Lstat(filepath.Dir(path), &dir); err != nil {
+ return nil, err
+ }
+ if !parent.IsDir() || parent.Mode()&os.ModeSymlink != 0 || parent.Mode().Perm()&0022 != 0 || int(dir.Uid) != os.Geteuid() {
+ return nil, ErrClaimStopIdentity
+ }
+ if err := removeStaleFilesSeedSocket(ctx, path); err != nil {
+ return nil, err
+ }
+ listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
+ if err != nil {
+ return nil, err
+ }
+ if err := os.Chmod(path, 0600); err != nil {
+ listener.Close()
+ return nil, err
+ }
+ if err := ctx.Err(); err != nil {
+ listener.Close()
+ return nil, err
+ }
+ live, cancel := context.WithCancel(ctx)
+ live, finish := activator.WithCleanupBudget(live, 30*time.Second)
+ s := &filesSeedServer{listener: listener, cancel: cancel, done: make(chan struct{})}
+ stop := context.AfterFunc(live, func() { listener.Close() })
+ go func() {
+ defer close(s.done)
+ defer finish()
+ defer stop()
+ defer listener.Close()
+ busy := make(chan struct{}, 1)
+ for {
+ conn, err := listener.AcceptUnix()
+ if err != nil {
+ break
+ }
+ select {
+ case busy <- struct{}{}:
+ default:
+ conn.Close()
+ continue
+ }
+ s.handlers.Add(1)
+ go func() {
+ defer s.handlers.Done()
+ defer func() { <-busy }()
+ defer conn.Close()
+ serveFilesSeed(live, conn, auth, run)
+ }()
+ }
+ s.handlers.Wait()
+ }()
+ return s, nil
+}
+
+func (s *filesSeedServer) close(ctx context.Context) error {
+ if s == nil {
+ return nil
+ }
+ s.cancel()
+ s.listener.Close()
+ select {
+ case <-s.done:
+ if s.finalize != nil {
+ return s.finalize(ctx)
+ }
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+func serveFilesSeed(parent context.Context, conn *net.UnixConn, auth func(context.Context, *net.UnixConn) error, run func(context.Context, string, io.Writer) error) {
+ ctx, cancel := context.WithCancel(parent)
+ defer cancel()
+ ctx, finish := activator.WithCleanupBudget(ctx, 30*time.Second)
+ defer finish()
+ stop := context.AfterFunc(ctx, func() { conn.Close() })
+ defer stop()
+ conn.SetReadDeadline(time.Now().Add(5 * time.Second))
+ if err := auth(ctx, conn); err != nil {
+ return
+ }
+ reader := bufio.NewReaderSize(conn, 1024)
+ line, err := reader.ReadSlice('\n')
+ if err != nil || len(line) > 1024 {
+ return
+ }
+ var q filesSeedRequest
+ decoder := json.NewDecoder(bytes.NewReader(line))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&q); err != nil {
+ return
+ }
+ // Canonical encoding also rejects duplicate fields and trailing JSON.
+ canonical, _ := json.Marshal(q)
+ var compact bytes.Buffer
+ if json.Compact(&compact, line) != nil || !bytes.Equal(compact.Bytes(), canonical) {
+ return
+ }
+ if q.Verb != "seed" {
+ return
+ }
+ if q.Host != "" {
+ if _, err := hostclaim.Key(q.Host); err != nil {
+ return
+ }
+ }
+ if err := auth(ctx, conn); err != nil {
+ return
+ }
+ conn.SetReadDeadline(time.Time{})
+ monitor := make(chan struct{})
+ go func() { defer close(monitor); _, _ = reader.ReadByte(); cancel() }()
+ stream := filesSeedStream{conn: conn, encoder: json.NewEncoder(conn)}
+ result := run(ctx, q.Host, stream)
+ code := filesSeedExitCode(result)
+ if result != nil {
+ _, _ = stream.Write([]byte(result.Error() + "\n"))
+ }
+ conn.SetWriteDeadline(time.Now().Add(time.Second))
+ _ = stream.encoder.Encode(filesSeedReply{Code: &code})
+ conn.Close()
+ <-monitor
+}
+
+func filesSeedClient(ctx context.Context, host string, out io.Writer, uid int) error {
+ var st unix.Stat_t
+ if err := unix.Lstat(filesSeedSocket, &st); err != nil {
+ return errors.Join(errFilesSeedDelegation, err)
+ }
+ if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Mode&0777 != 0600 || st.Uid != uint32(uid) {
+ return ErrClaimStopIdentity
+ }
+ conn, err := (&net.Dialer{}).DialContext(ctx, "unix", filesSeedSocket)
+ if err != nil {
+ return errors.Join(errFilesSeedDelegation, err)
+ }
+ defer conn.Close()
+ stop := context.AfterFunc(ctx, func() { conn.Close() })
+ defer stop()
+ if err := authenticateFilesSeedPeer(ctx, conn.(*net.UnixConn), uid, true); err != nil {
+ return err
+ }
+ var after unix.Stat_t
+ if err := unix.Lstat(filesSeedSocket, &after); err != nil || st.Dev != after.Dev || st.Ino != after.Ino {
+ return ErrClaimStopIdentity
+ }
+ if err := json.NewEncoder(conn).Encode(filesSeedRequest{Verb: "seed", Host: host}); err != nil {
+ return err
+ }
+ reader := bufio.NewReaderSize(conn, 64*1024)
+ for {
+ line, err := reader.ReadSlice('\n')
+ if err != nil {
+ return err
+ }
+ var reply filesSeedReply
+ if err := json.Unmarshal(line, &reply); err != nil {
+ return err
+ }
+ if reply.Text != "" {
+ if _, err := io.WriteString(out, reply.Text); err != nil {
+ return err
+ }
+ }
+ if reply.Code != nil {
+ switch *reply.Code {
+ case 0:
+ return nil
+ case 2:
+ return filesSeedIncompleteError{hostbox.ErrChanged}
+ default:
+ return hostbox.ErrChanged
+ }
+ }
+ }
+}
+
+func startAgentFilesSeed(ctx context.Context, ready <-chan struct{}, svc *activator.Service) (*filesSeedServer, error) {
+ entry, _ := ctx.Value(claimEntryKey{}).(claimEntry)
+ resources := entry.Resources
+ source, ok := productionClaimFilesSource().(*movesFilesSource)
+ if !ok || entry.Assembly == nil || svc == nil || resources.Manager == nil {
+ return nil, hostbox.ErrUnsupported
+ }
+ if err := NewClaimRoleRootClient(controlsock.Path).KindCapability(ctx, "files-seed"); err != nil {
+ return nil, err
+ }
+ executor := &filesSeedExecutor{assembly: entry.Assembly, service: svc, registry: resources.Registry, source: source}
+ server, err := startFilesSeedSocket(ctx, ready, filesSeedSocket, func(ctx context.Context, c *net.UnixConn) error {
+ return authenticateFilesSeedPeer(ctx, c, os.Geteuid(), false)
+ }, executor.run)
+ if server != nil {
+ server.finalize = executor.close
+ }
+ return server, err
+}
+
+// Recovery removes only an unchanged, service-owned socket which the kernel
+// refused to connect to. A live or uncertain listener always wins.
+func removeStaleFilesSeedSocket(ctx context.Context, path string) error {
+ var before unix.Stat_t
+ err := unix.Lstat(path, &before)
+ if errors.Is(err, unix.ENOENT) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if before.Mode&unix.S_IFMT != unix.S_IFSOCK || before.Mode&0777 != 0600 || int(before.Uid) != os.Geteuid() {
+ return ErrClaimStopIdentity
+ }
+ conn, err := (&net.Dialer{Timeout: time.Second}).DialContext(ctx, "unix", path)
+ if err == nil {
+ conn.Close()
+ return hostbox.ErrChanged
+ }
+ if !errors.Is(err, unix.ECONNREFUSED) {
+ return err
+ }
+ var after unix.Stat_t
+ if err := unix.Lstat(path, &after); err != nil {
+ return err
+ }
+ if before.Dev != after.Dev || before.Ino != after.Ino || before.Ctim != after.Ctim {
+ return ErrClaimStopIdentity
+ }
+ return os.Remove(path)
+}
diff --git a/internal/host/claimfilesseed_transport_linux_test.go b/internal/host/claimfilesseed_transport_linux_test.go
new file mode 100644
index 000000000..400589865
--- /dev/null
+++ b/internal/host/claimfilesseed_transport_linux_test.go
@@ -0,0 +1,244 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net"
+ "os"
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+ "time"
+)
+
+func TestFilesSeedSocketStartupDisconnectAndShutdown(t *testing.T) {
+ for _, canceled := range []bool{false, true} {
+ t.Run(map[bool]string{true: "canceled", false: "ready"}[canceled], func(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ ready := make(chan struct{})
+ dir := t.TempDir()
+ if err := os.Chmod(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(dir, "seed.sock")
+ entered, joined := make(chan struct{}), make(chan struct{})
+ var calls atomic.Int32
+ run := func(ctx context.Context, host string, out io.Writer) error {
+ calls.Add(1)
+ close(entered)
+ <-ctx.Done()
+ close(joined)
+ return ctx.Err()
+ }
+ result := make(chan *filesSeedServer, 1)
+ go func() {
+ s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, run)
+ result <- s
+ }()
+ select {
+ case <-result:
+ t.Fatal("bound before startup")
+ case <-time.After(20 * time.Millisecond):
+ }
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("early socket", err)
+ }
+ if canceled {
+ cancel()
+ }
+ close(ready)
+ s := <-result
+ if canceled {
+ if s != nil {
+ t.Fatal("late socket")
+ }
+ return
+ }
+ if s == nil {
+ t.Fatal("no socket")
+ }
+ c, err := net.Dial("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed", Host: recoveryHost}); err != nil {
+ t.Fatal(err)
+ }
+ <-entered
+ c.Close()
+ select {
+ case <-joined:
+ case <-time.After(time.Second):
+ t.Fatal("disconnect detached request")
+ }
+ cancel()
+ join, cancelJoin := context.WithTimeout(context.Background(), time.Second)
+ defer cancelJoin()
+ if err := s.close(join); err != nil {
+ t.Fatal(err)
+ }
+ if calls.Load() != 1 {
+ t.Fatal(calls.Load())
+ }
+ if _, err := net.Dial("unix", path); err == nil {
+ t.Fatal("listener survived")
+ }
+ })
+ }
+}
+
+func TestFilesSeedSocketRefusesMalformedAndUntrusted(t *testing.T) {
+ for _, body := range []string{`{"verb":"seed","host":"../bad"}`, `{"verb":"repair"}`, `{"verb":"seed","pid":1}`, `{"verb":"seed","host":"a","host":"b"}`} {
+ t.Run(body, func(t *testing.T) {
+ ready := make(chan struct{})
+ close(ready)
+ dir := t.TempDir()
+ if err := os.Chmod(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(dir, "seed.sock")
+ s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { t.Error("malformed request executed"); return nil })
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.close(t.Context())
+ c, err := net.Dial("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer c.Close()
+ c.SetDeadline(time.Now().Add(time.Second))
+ io.WriteString(c, body+"\n")
+ var r filesSeedReply
+ if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 {
+ t.Fatal("accepted")
+ }
+ })
+ }
+ ready := make(chan struct{})
+ close(ready)
+ dir := t.TempDir()
+ if err := os.Chmod(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(dir, "seed.sock")
+ s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return errors.New("refused") }, func(context.Context, string, io.Writer) error { t.Error("untrusted request executed"); return nil })
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.close(t.Context())
+ c, err := net.Dial("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer c.Close()
+ json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
+ var r filesSeedReply
+ if json.NewDecoder(c).Decode(&r) == nil && r.Code != nil && *r.Code == 0 {
+ t.Fatal("untrusted accepted")
+ }
+ if _, err := startFilesSeedSocket(t.Context(), ready, path, nil, nil); err == nil {
+ t.Fatal("replaced live socket")
+ }
+}
+
+func TestFilesSeedSocketStaleRecoveryAndBoundedJoin(t *testing.T) {
+ dir := t.TempDir()
+ os.Chmod(dir, 0700)
+ path := filepath.Join(dir, "seed.sock")
+ old, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ old.SetUnlinkOnClose(false)
+ os.Chmod(path, 0600)
+ old.Close()
+ ready := make(chan struct{})
+ close(ready)
+ entered, release := make(chan struct{}), make(chan struct{})
+ s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { close(entered); <-release; return nil })
+ if err != nil {
+ t.Fatal(err)
+ }
+ var closed atomic.Bool
+ s.finalize = func(context.Context) error { closed.Store(true); return nil }
+ c, err := net.Dial("unix", path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer c.Close()
+ json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
+ <-entered
+ if err := removeStaleFilesSeedSocket(t.Context(), path); err == nil {
+ t.Fatal("live listener removed")
+ }
+ deadline, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond)
+ defer cancel()
+ if err := s.close(deadline); !errors.Is(err, context.DeadlineExceeded) || closed.Load() {
+ t.Fatal("unjoined runtime allowed close", err)
+ }
+ close(release)
+ if err := s.close(t.Context()); err != nil || !closed.Load() {
+ t.Fatal("joined runtime not finalized", err)
+ }
+}
+
+func TestFilesSeedSocketWaitsConfigureAndStart(t *testing.T) {
+ for _, phase := range []string{"configure", "start", "ready"} {
+ t.Run(phase, func(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ dir := t.TempDir()
+ os.Chmod(dir, 0700)
+ path := filepath.Join(dir, "seed.sock")
+ configured, allowConfigure, starting, allowStart, ready := make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{}), make(chan struct{})
+ startup := make(chan struct{})
+ go func() {
+ defer close(startup)
+ <-(activatorStart{Claims: true, Configure: func(context.Context) { close(configured); <-allowConfigure }, Start: func() { close(starting); <-allowStart }, Ready: func() { close(ready) }}).Run(ctx)
+ }()
+ result := make(chan *filesSeedServer, 1)
+ go func() {
+ s, _ := startFilesSeedSocket(ctx, ready, path, func(context.Context, *net.UnixConn) error { return nil }, func(context.Context, string, io.Writer) error { return nil })
+ result <- s
+ }()
+ <-configured
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("socket during Configure")
+ }
+ if phase == "configure" {
+ cancel()
+ }
+ close(allowConfigure)
+ if phase != "configure" {
+ <-starting
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("socket during Start")
+ }
+ if phase == "start" {
+ cancel()
+ }
+ }
+ close(allowStart)
+ <-startup
+ s := <-result
+ if phase != "ready" {
+ if s != nil {
+ t.Fatal("socket after canceled startup")
+ }
+ return
+ }
+ if s == nil {
+ t.Fatal("no socket after readiness")
+ }
+ if err := s.close(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
diff --git a/internal/host/claimfilesseed_transport_other.go b/internal/host/claimfilesseed_transport_other.go
new file mode 100644
index 000000000..1e916d19b
--- /dev/null
+++ b/internal/host/claimfilesseed_transport_other.go
@@ -0,0 +1,17 @@
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/activator"
+ "io"
+)
+
+type filesSeedServer struct{}
+
+func (*filesSeedServer) close(context.Context) error { return nil }
+func startAgentFilesSeed(context.Context, <-chan struct{}, *activator.Service) (*filesSeedServer, error) {
+ return nil, errFilesSeedDelegation
+}
+func filesSeedClient(context.Context, string, io.Writer, int) error { return errFilesSeedDelegation }
diff --git a/internal/host/claimlaunch_production.go b/internal/host/claimlaunch_production.go
index 0183c9b99..07a737920 100644
--- a/internal/host/claimlaunch_production.go
+++ b/internal/host/claimlaunch_production.go
@@ -45,6 +45,8 @@ func (h ClaimRootWithLaunch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Launch.ServeHTTP(w, r)
}
+func (h ClaimRootWithLaunch) Start(ctx context.Context) { h.Root.Start(ctx) }
+
func (h ClaimRootWithLaunch) RecoverPending(ctx context.Context) error {
return h.Root.RecoverPending(ctx)
}
diff --git a/internal/host/claimrecovery_rulings_test.go b/internal/host/claimrecovery_rulings_test.go
new file mode 100644
index 000000000..ede38fb5e
--- /dev/null
+++ b/internal/host/claimrecovery_rulings_test.go
@@ -0,0 +1,82 @@
+package host
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+func TestWrappedClaimRootStartsAndServes(t *testing.T) {
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ var handler http.Handler = ClaimRootWithLaunch{Root: ClaimRootHTTP{}}
+ lifecycle, ok := handler.(interface{ Start(context.Context) })
+ if !ok {
+ t.Fatal("wrapped handler lost lifecycle")
+ }
+ lifecycle.Start(ctx)
+ r := httptest.NewRecorder()
+ handler.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/claim/retire", nil))
+ if r.Code != http.StatusServiceUnavailable {
+ t.Fatal(r.Code)
+ }
+}
+
+type recoveryPages struct {
+ servingParkAuthority
+ calls []string
+}
+
+func (p *recoveryPages) recoverServingParks(_ context.Context, after string, limit int) (servingParkRecoveryPage, error) {
+ p.calls = append(p.calls, after)
+ page := servingParkRecoveryPage{}
+ count := 128
+ if after != "" {
+ count = 72
+ }
+ for i := 0; i < count; i++ {
+ page.Results = append(page.Results, servingParkRecoveryResult{Host: "kept", ID: "park", Err: errServingParkRetagPending})
+ }
+ if after == "" {
+ page.More = true
+ page.Next = "128"
+ }
+ return page, nil
+}
+func TestServingParkRecoveryPagesRetainedRecords(t *testing.T) {
+ a := &recoveryPages{}
+ ctx, cancel := context.WithCancel(t.Context())
+ defer cancel()
+ p := &servingParkRuntime{authority: a, ctx: ctx, cancel: cancel, idle: make(chan struct{})}
+ for i := 0; i < 2; i++ {
+ if err := p.recoverPending(ctx); err != nil {
+ t.Fatal("retained parks blocked recovery", err)
+ }
+ }
+ if len(a.calls) != 2 || a.calls[0] != "" || a.calls[1] != "128" {
+ t.Fatal(a.calls)
+ }
+}
+func TestServingParkPressurePrecedesSlowRecovery(t *testing.T) {
+ cleaned := false
+ ctx, cancel := context.WithTimeout(t.Context(), time.Second)
+ defer cancel()
+ _, err := maintainServingParkPass(ctx, "", func(call context.Context) error {
+ if call.Err() != nil {
+ t.Fatal(call.Err())
+ }
+ cleaned = true
+ return nil
+ }, func(call context.Context, _ string, _ int) (servingParkRecoveryPage, error) {
+ if !cleaned {
+ t.Fatal("recovery starved pressure cleanup")
+ }
+ <-call.Done()
+ return servingParkRecoveryPage{}, call.Err()
+ }, 10*time.Millisecond)
+ if !cleaned || err == nil {
+ t.Fatal(cleaned, err)
+ }
+}
diff --git a/internal/host/claimretire_transport.go b/internal/host/claimretire_transport.go
index 9b9f6ff88..d8868b02d 100644
--- a/internal/host/claimretire_transport.go
+++ b/internal/host/claimretire_transport.go
@@ -34,39 +34,30 @@ type ClaimRootHTTP struct {
Roots map[string]string
}
-func (h ClaimRootHTTP) RecoverPending(ctx context.Context) (recoveryErr error) {
- // Serving intents retain their own authority checks even when another
- // startup recovery failed. Attempt them before the service accepts work.
- defer func() { recoveryErr = errors.Join(recoveryErr, h.servingParks.recoverPending(ctx)) }()
+func (h ClaimRootHTTP) Start(ctx context.Context) {
+ if h.executionRuntime != nil {
+ h.executionRuntime.Start(ctx)
+ }
+ h.servingParks.Start(ctx)
+}
- if err := h.servingHandover().Recover(ctx); err != nil {
- return err
+func (h ClaimRootHTTP) RecoverPending(ctx context.Context) (recoveryErr error) {
+ // Controller recovery is independent of per-host retained journals and parks.
+ if h.Execution != nil {
+ recoveryErr = h.Execution.Recover(ctx)
}
+ recoveryErr = errors.Join(recoveryErr, h.Stop.RecoverPending(ctx))
+ recoveryErr = errors.Join(recoveryErr, h.servingHandover().Recover(ctx))
cold := hostbox.ColdHandover{OperationsOnly: true, Guard: h.Stop.Coordinator.Guard, Claims: h.Stop.Coordinator.Evidence, Registry: h.Reconcile.Registry, Inventory: h.Reconcile.Inventory, Launches: h.Launches, Roots: h.Roots, JournalDir: filepath.Join(h.Reconcile.Retirement.JournalDir, "cold"), VerifyReceipt: verifyRetirementReceipt}
- if err := cold.Recover(ctx); err != nil {
- return err
- }
- if err := h.Stop.RecoverPending(ctx); err != nil {
- return err
- }
+ recoveryErr = errors.Join(recoveryErr, cold.Recover(ctx))
journals, err := hostbox.SavedRetirementJournals(h.Reconcile.Retirement.JournalDir)
- if err != nil {
- return err
- }
+ recoveryErr = errors.Join(recoveryErr, err)
for _, journal := range journals {
- if err = h.retire(ctx, *journal.Target); err != nil {
- return err
- }
- }
- if err := h.recoverRootInventory(ctx); err != nil {
- return err
- }
- if h.Execution != nil {
- // Retained failures deny their host; recovery must not remove the
- // socket needed to finish their existing stop protocol.
- _ = h.Execution.Recover(ctx)
+ recoveryErr = errors.Join(recoveryErr, h.retire(ctx, *journal.Target))
}
- return ctx.Err()
+ recoveryErr = errors.Join(recoveryErr, h.recoverRootInventory(ctx))
+ recoveryErr = errors.Join(recoveryErr, h.servingParks.recoverPending(ctx))
+ return errors.Join(recoveryErr, ctx.Err())
}
func (h ClaimRootHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/claim/execution" && r.URL.Path != "/claim/recovery-rebind" && r.URL.Path != "/claim/retire" && r.URL.Path != "/claim/handover" && r.URL.Path != "/claim/journal" {
diff --git a/internal/host/claimservingmaintenance.go b/internal/host/claimservingmaintenance.go
index a26fcc34a..03dd44719 100644
--- a/internal/host/claimservingmaintenance.go
+++ b/internal/host/claimservingmaintenance.go
@@ -132,14 +132,16 @@ func (p *servingParkRuntime) Start(ctx context.Context) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
after := ""
+ lastLog := time.Time{}
for {
- bounded, cancel := context.WithTimeout(call, 10*time.Second)
+ bounded, cancel := context.WithCancel(call)
if a, ok := p.authority.(interface {
maintainServingParks(context.Context, string) (string, error)
}); ok {
var err error
after, err = a.maintainServingParks(bounded, after)
- if err != nil && call.Err() == nil {
+ if err != nil && call.Err() == nil && time.Since(lastLog) >= time.Hour {
+ lastLog = time.Now()
log.Print(err)
}
}
@@ -152,3 +154,20 @@ func (p *servingParkRuntime) Start(ctx context.Context) {
}
}()
}
+
+func maintainServingParkPass(ctx context.Context, after string, cleanup func(context.Context) error, recover func(context.Context, string, int) (servingParkRecoveryPage, error), budget time.Duration) (string, error) {
+ pressure, stopPressure := context.WithTimeout(ctx, budget)
+ pressureErr := cleanup(pressure)
+ stopPressure()
+ recovery, stopRecovery := context.WithTimeout(ctx, budget)
+ defer stopRecovery()
+ page, err := recover(recovery, after, servingParkRecoveryLimit)
+ for _, result := range page.Results {
+ err = errors.Join(err, result.Err)
+ }
+ next := ""
+ if page.More {
+ next = page.Next
+ }
+ return next, errors.Join(pressureErr, err)
+}
diff --git a/internal/host/claimservingmaintenance_linux.go b/internal/host/claimservingmaintenance_linux.go
index 852c6a2a3..dd6ebebbc 100644
--- a/internal/host/claimservingmaintenance_linux.go
+++ b/internal/host/claimservingmaintenance_linux.go
@@ -4,9 +4,9 @@ package host
import (
"context"
- "errors"
"github.com/carlosframework/platform/internal/instancefs"
"os"
+ "time"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/instancectl"
@@ -37,19 +37,11 @@ func servingRoomFile(parent *os.File) error {
}
func (p *servingParkRoot) maintainServingParks(ctx context.Context, after string) (string, error) {
- page, err := p.recoverServingParks(ctx, after, servingParkRecoveryLimit)
- for _, result := range page.Results {
- err = errors.Join(err, result.Err)
- }
- next := ""
- if page.More {
- next = page.Next
- }
fs, ok := p.ledger.fs.(*servingParkFS)
if !ok {
- return next, hostbox.ErrUnsupported
+ return after, hostbox.ErrUnsupported
}
- return next, errors.Join(err, p.ledger.maintainParks(ctx, fs.room))
+ return maintainServingParkPass(ctx, after, func(call context.Context) error { return p.ledger.maintainParks(call, fs.room) }, p.recoverServingParks, 10*time.Second)
}
func claimServingRoom(data string) error {
diff --git a/internal/host/claimservingpark_reap_test.go b/internal/host/claimservingpark_reap_test.go
new file mode 100644
index 000000000..c8318da3d
--- /dev/null
+++ b/internal/host/claimservingpark_reap_test.go
@@ -0,0 +1,32 @@
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostbox"
+)
+
+// reapLocked needs only namespace/slot/record authority; it never takes a host
+// guard. Pressure selection and authenticated replacement are separate callers.
+func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error {
+ r, err := l.read(id)
+ if err != nil {
+ return err
+ }
+ unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlock()
+ unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlockRecord()
+ if r.Closed {
+ return nil
+ }
+ if !r.Complete || r.Disposition != servingParkRetain || l.now().Before(r.ReapAfter) {
+ return hostbox.ErrChanged
+ }
+ return l.deleteLocked(ctx, &r, "expiry")
+}
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go
index e4c06c985..b1a7901c7 100644
--- a/internal/host/claimservingpark_records.go
+++ b/internal/host/claimservingpark_records.go
@@ -658,32 +658,6 @@ func (l *servingParkLedger) deleteLocked(ctx context.Context, r *servingParkReco
return save()
}
-// reapLocked needs only namespace/slot/record authority; it never takes a host
-// guard. Pressure selection and authenticated replacement are separate callers.
-func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error {
- r, err := l.read(id)
- if err != nil {
- return err
- }
- unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock")
- if err != nil {
- return err
- }
- defer unlock()
- unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
- if err != nil {
- return err
- }
- defer unlockRecord()
- if r.Closed {
- return nil
- }
- if !r.Complete || r.Disposition != servingParkRetain || l.now().Before(r.ReapAfter) {
- return hostbox.ErrChanged
- }
- return l.deleteLocked(ctx, &r, "expiry")
-}
-
func (l *servingParkLedger) replaceLocked(ctx context.Context, id string) error {
unlock, err := l.state.Lock(ctx, "retirement-"+id+".lock")
if err != nil {
diff --git a/internal/host/claimservingpark_recovery.go b/internal/host/claimservingpark_recovery.go
index 278e92a51..b0e594a4f 100644
--- a/internal/host/claimservingpark_recovery.go
+++ b/internal/host/claimservingpark_recovery.go
@@ -4,8 +4,10 @@ import (
"context"
"errors"
"fmt"
+ "log"
"sort"
"strings"
+ "time"
"github.com/carlosframework/platform/internal/hostbox"
)
@@ -126,7 +128,15 @@ func (p *servingParkRuntime) recoverPending(ctx context.Context) error {
if !ok {
return hostbox.ErrUnsupported
}
- page, err := recovery.recoverServingParks(call, "", servingParkRecoveryLimit)
+ p.recoveryMu.Lock()
+ defer p.recoveryMu.Unlock()
+ page, err := recovery.recoverServingParks(call, p.recoveryAfter, servingParkRecoveryLimit)
+ if err == nil {
+ p.recoveryAfter = ""
+ if page.More {
+ p.recoveryAfter = page.Next
+ }
+ }
var failures []error
if err != nil {
failures = append(failures, hostbox.ErrChanged)
@@ -146,8 +156,9 @@ func (p *servingParkRuntime) recoverPending(ctx context.Context) error {
failures = append(failures, fmt.Errorf("Host %q: serving database retirement %q retained: %s.", result.Host, result.ID, reason))
}
}
- if page.More {
- failures = append(failures, hostbox.ErrChanged)
+ if len(failures) > 0 && time.Since(p.lastRecoveryLog) >= time.Hour {
+ log.Print(errors.Join(failures...))
+ p.lastRecoveryLog = time.Now()
}
- return errors.Join(failures...)
+ return errors.Join(err, call.Err())
}
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go
index d6a8bb932..e348c8fc0 100644
--- a/internal/host/claimservingpark_rootrunner_linux_test.go
+++ b/internal/host/claimservingpark_rootrunner_linux_test.go
@@ -682,6 +682,10 @@ func TestRootRebootServingParkAcceptancePhase2(t *testing.T) {
}
func TestRootFilesSeedRealOriginPreparation(t *testing.T) {
+ runRootFilesSeedRealOriginPreparation(t, false)
+}
+func TestRootFilesSeedRealOriginTenant(t *testing.T) { runRootFilesSeedRealOriginPreparation(t, true) }
+func runRootFilesSeedRealOriginPreparation(t *testing.T, live bool) {
servingRootRunner(t)
if os.Getenv("PLAN14_ROOT_FIXTURE") == t.Name() {
if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
@@ -690,7 +694,11 @@ func TestRootFilesSeedRealOriginPreparation(t *testing.T) {
if !plan14RootFixture(t) {
t.Fatal("root child did not enter its scratch root")
}
- runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller", true)
+ if live {
+ runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", []bool{true}, []filesSeedLiveWitness{rootFilesSeedLiveWitness(t, "/tenant.test")})
+ } else {
+ runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller", true)
+ }
return
}
root := t.TempDir()
@@ -706,6 +714,15 @@ func TestRootFilesSeedRealOriginPreparation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
+ if live {
+ body, err := os.ReadFile(binary)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(root, "tenant.test"), body, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
cmd := exec.CommandContext(t.Context(), binary, "-test.run=^"+t.Name()+"$", "-test.v", "-test.timeout=3m")
cmd.Env = append(os.Environ(), "PLAN14_ROOT_FIXTURE="+t.Name(), "PLAN14_ROOT_DIR="+root)
cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: unix.CLONE_NEWNS}
diff --git a/internal/host/claimservingpark_transport.go b/internal/host/claimservingpark_transport.go
index 9bf5d1145..42ae7040c 100644
--- a/internal/host/claimservingpark_transport.go
+++ b/internal/host/claimservingpark_transport.go
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"sync"
+ "time"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
@@ -38,6 +39,9 @@ type servingParkRuntime struct {
cancel context.CancelFunc
mu sync.Mutex
active int
+ recoveryMu sync.Mutex
+ recoveryAfter string
+ lastRecoveryLog time.Time
closing, closed, started bool
idle chan struct{}
}
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
index 4a00ef143..6d6883a2c 100644
--- a/internal/host/claimservingpeer_rootrunner_linux_test.go
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -3,6 +3,7 @@
package host
import (
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -18,14 +19,17 @@ import (
"strconv"
"strings"
"sync/atomic"
+ "syscall"
"testing"
"time"
+ "github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/cgroup"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/hostguard"
"github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/rootstate"
"golang.org/x/sys/unix"
)
@@ -95,6 +99,12 @@ func TestRootServingParkPeerPayload(t *testing.T) {
}
client := hostbox.ServingParkClient{Call: call}
var manager *cgroup.Manager
+ var seedServer *filesSeedServer
+ defer func() {
+ if seedServer != nil {
+ seedServer.close(t.Context())
+ }
+ }()
for {
var q servingPeerCommand
if err := decode.Decode(&q); errors.Is(err, io.EOF) {
@@ -105,6 +115,25 @@ func TestRootServingParkPeerPayload(t *testing.T) {
var reply servingPeerReply
var callErr error
switch q.Action {
+ case "seed-listen":
+ ready := make(chan struct{})
+ close(ready)
+ seedServer, callErr = startFilesSeedSocket(t.Context(), ready, filesSeedSocket, func(ctx context.Context, c *net.UnixConn) error {
+ return authenticateFilesSeedPeer(ctx, c, os.Geteuid(), false)
+ }, func(ctx context.Context, host string, out io.Writer) error {
+ _, err := io.WriteString(out, "accepted "+host+"\n")
+ return err
+ })
+ case "seed-flat-client":
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestRootFilesSeedClientPayload$", "-test.v")
+ cmd.Env = append(os.Environ(), "FILES_SEED_EXPECT_REFUSAL=1", "FILES_SEED_CLIENT=1")
+ if out, err := cmd.CombinedOutput(); err != nil {
+ callErr = fmt.Errorf("flat client: %w: %s", err, out)
+ }
case "seed-manager":
manager, callErr = filesSeedManager(t.Context())
case "seed-capability":
@@ -312,6 +341,9 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
t.Fatal(err)
}
f.p.root.Stop = production.(ClaimStopHTTP)
+ if namespace == "seed-killed" {
+ f.p.root.Stop.Coordinator.Kernel = servingSeedRecoveryTrace{LinuxStopKernel: f.p.root.Stop.Coordinator.Kernel.(LinuxStopKernel), t: t}
+ }
kernelResults := make(chan error, 8)
if writer {
kernel := f.p.root.Stop.Coordinator.Kernel
@@ -342,7 +374,7 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
t.Fatal(err)
}
var handler http.Handler = h
- if namespace == "seed-delegation" {
+ if strings.HasPrefix(namespace, "seed-") {
capability := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}).root
capability.Authenticate = h.Stop.Authenticate
capability.SeedController = verifyFilesSeedDelegation
@@ -350,6 +382,9 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
}
server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
ctx, cancel := context.WithCancel(context.Background())
+ if lifecycle, ok := handler.(interface{ Start(context.Context) }); ok {
+ lifecycle.Start(ctx)
+ }
done := make(chan error, 1)
go func() { done <- server.Serve(ctx, listener) }()
t.Cleanup(func() {
@@ -380,6 +415,18 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
}
// Add a Service section because the embedded unit ends in Install.
content := strings.Join(lines, "\n") + fmt.Sprintf("\n[Service]\nType=simple\nRestart=no\nTimeoutStopSec=10s\nExecStart=%s -test.run=^TestRootServingParkPeerPayload$ -test.v -test.timeout=2m\nEnvironment=SERVING_PEER_CONTROL=%s SERVING_PEER_SOCKET=%s\nIPAddressDeny=any\n", installed, controlPath, path)
+ if namespace == "seed-live" {
+ if err := os.Mkdir("/run/carlos", 0755); err != nil {
+ t.Fatal("refusing existing runtime directory", err)
+ }
+ t.Cleanup(func() { os.RemoveAll("/run/carlos") })
+ uid, _ := strconv.Atoi(account.Uid)
+ gid, _ := strconv.Atoi(account.Gid)
+ if err := os.Chown("/run/carlos", uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ content += "ReadWritePaths=/run/carlos\n"
+ }
unitPath := "/run/systemd/system/carlos-edge.service"
unit, err := os.OpenFile(unitPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
@@ -452,10 +499,83 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
}
return reply
}
- if namespace == "seed-delegation" {
+ if strings.HasPrefix(namespace, "seed-") {
command(servingPeerCommand{Action: "seed-capability"}, true)
command(servingPeerCommand{Action: "seed-manager"}, false)
command(servingPeerCommand{Action: "seed-capability"}, false)
+ if namespace == "seed-killed" {
+ observed := f.f.read()
+ attempt := hostclaim.WakeRetryAttempt{ID: "killed-attempt", Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: next.BootID, ControllerID: next.ControllerID, Session: observed.Claim.Holder.Session, InvocationID: "killed-invocation", LaunchInputs: "seed-inputs"}
+ observed, err = hostclaim.Update(t.Context(), f.f.claims, observed, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: observed.Claim.Holder.Incarnation, EpisodeID: "killed-seed", Attempt: attempt, Now: f.f.now}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ target := recoveryTuple(observed, claimStopLocal{Actor: f.actor})
+ pre := command(servingPeerCommand{Action: "prelaunch", Target: target}, false)
+ former := next
+ servingRunnerSystemctl(t, "kill", "--signal=KILL", "carlos-edge.service")
+ // Start waits for systemd to reap the killed MainPID; no graceful cleanup ran.
+ servingRunnerSystemctl(t, "stop", "carlos-edge.service")
+ _, encode, decode, next = connect()
+ if former.PID == next.PID || former.ControllerID == next.ControllerID {
+ t.Fatal("kill did not replace controller")
+ }
+ command(servingPeerCommand{Action: "seed-manager"}, false)
+ command(servingPeerCommand{Action: "seed-capability"}, false)
+ rt := registry.Route{Host: observed.Host, DBPath: f.db}
+ files := &movesFiles{DataDir: f.f.guard.DataDir, EnvFile: func(host, db string) (claimEnvFile, error) {
+ return claimEnvFile{Nested: true, Dir: filepath.Dir(db), Name: host + ".env"}, nil
+ }}
+ service := &activator.Service{}
+ exclusion, err := service.AcquireSeedExclusion(t.Context(), rt.Host, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer exclusion.Release()
+ stops := 0
+ retries := activator.ServingRetries{Claims: f.f.claims, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: next.BootID, ControllerID: next.ControllerID, Now: func() time.Time { return f.f.now }, NewID: f.f.newID, Inputs: func(context.Context, registry.Route) (string, error) { return "seed-inputs", nil }, ExactHealth: func(context.Context, *exec.Cmd, hostclaim.WakeRetryAttempt) error { return hostbox.ErrChanged }, StopAttempt: func(ctx context.Context, o hostclaim.Observed, a hostclaim.WakeRetryAttempt) error {
+ stops++
+ req := StopRequest{Region: f.actor.Region, Recover: true, Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: o.Claim.Holder.Stamp, WriterSession: o.Claim.Holder.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: a.BootID, ControllerID: a.ControllerID, ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: pre.Launch.Target.DirectoryRevision}
+ command(servingPeerCommand{Action: "stop", Stop: req}, false)
+ return nil
+ }}
+ settled, err := settleFilesSeedRerun(t.Context(), rt, files, retries, observed, exclusion)
+ if err != nil {
+ t.Fatal("killed rerun settlement", err)
+ }
+ retry := settled.Claim.Holder.WakeRetry
+ if retry == nil || retry.State != "open" || retry.AttemptsReserved != 1 || retry.Attempt != nil || stops != 1 {
+ t.Fatal("retry debt not settled exactly", retry, stops)
+ }
+ again, err := settleFilesSeedRerun(t.Context(), rt, files, retries, settled, exclusion)
+ if err != nil || again.ETag != settled.ETag || stops != 1 {
+ t.Fatal("rerun repeated stop or reservation", err)
+ }
+ return
+ }
+ if namespace == "seed-live" {
+ command(servingPeerCommand{Action: "seed-listen"}, false)
+ uid, _ := strconv.Atoi(account.Uid)
+ gid, _ := strconv.Atoi(account.Gid)
+ cmd := exec.CommandContext(t.Context(), installed, "-test.run=^TestRootFilesSeedClientPayload$", "-test.v")
+ cmd.Dir = root
+ cmd.Env = append(os.Environ(), "FILES_SEED_CLIENT=1")
+ cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("external client: %v %s", err, out)
+ }
+ command(servingPeerCommand{Action: "seed-flat-client"}, false)
+ c, err := net.Dial("unix", filesSeedSocket)
+ if err != nil {
+ t.Fatal(err)
+ }
+ json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed"})
+ c.SetReadDeadline(time.Now().Add(time.Second))
+ if _, err := c.Read(make([]byte, 1)); err == nil {
+ t.Fatal("root client accepted")
+ }
+ c.Close()
+ }
if err := verifyFilesSeedDelegation(t.Context(), first); err == nil {
t.Fatal("former controller accepted")
}
@@ -683,3 +803,63 @@ func (p servingPeerParkTrace) completeServingDatabasePark(ctx context.Context, c
func TestRootFilesSeedDelegatedController(t *testing.T) {
runServingParkPeerNamespace(t, false, false, "seed-delegation")
}
+
+func TestRootFilesSeedLiveTransport(t *testing.T) {
+ runServingParkPeerNamespace(t, false, false, "seed-live")
+}
+
+func TestRootFilesSeedClientPayload(t *testing.T) {
+ if os.Getenv("FILES_SEED_CLIENT") != "1" {
+ t.Fatal("invoke through live transport witness")
+ }
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ t.Setenv("CARLOS_MOVES_PHASE1", "")
+ var out bytes.Buffer
+ err := runSystemFiles(t.Context(), []string{"seed", "--host", recoveryHost}, &out)
+ if os.Getenv("FILES_SEED_EXPECT_REFUSAL") == "1" {
+ if err == nil {
+ t.Fatal("tenant admitted")
+ }
+ return
+ }
+ if err != nil || out.String() != "accepted "+recoveryHost+"\n" {
+ t.Fatal(err, out.String())
+ }
+ // A same-UID impostor can own a pathname but cannot impersonate MainPID.
+ held := filesSeedSocket + ".held"
+ if err := os.Rename(filesSeedSocket, held); err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ if err := os.Rename(held, filesSeedSocket); err != nil {
+ t.Error(err)
+ }
+ }()
+ fake, err := net.ListenUnix("unix", &net.UnixAddr{Name: filesSeedSocket, Net: "unix"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer fake.Close()
+ if err := os.Chmod(filesSeedSocket, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := runSystemFiles(t.Context(), []string{"seed"}, io.Discard); !errors.Is(err, ErrClaimStopIdentity) {
+ t.Fatal("impostor accepted", err)
+ }
+}
+
+func TestRootFilesSeedKilledControllerRerun(t *testing.T) {
+ runServingParkPeerNamespace(t, false, false, "seed-killed")
+}
+
+type servingSeedRecoveryTrace struct {
+ LinuxStopKernel
+ t *testing.T
+}
+
+func (k servingSeedRecoveryTrace) RecoverStopped(ctx context.Context, target hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
+ evidence, ack, err := k.LinuxStopKernel.RecoverStopped(ctx, target, in)
+ k.t.Logf("killed controller root recovery: %v", err)
+ return evidence, ack, err
+}
diff --git a/internal/host/claimstop_kernel_linux.go b/internal/host/claimstop_kernel_linux.go
index 1ce3933c3..edbb0b1cf 100644
--- a/internal/host/claimstop_kernel_linux.go
+++ b/internal/host/claimstop_kernel_linux.go
@@ -315,6 +315,9 @@ func (k LinuxStopKernel) RecoverStopped(ctx context.Context, t hostguard.FenceTa
}
}
if _, err := scanStopCgroupsMode(ctx, k.CgroupRoot, j.Cgroups, false); err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return k.recoverUnlaunchedAbsent(ctx, t, in, j, journal)
+ }
return refuse(err)
}
groups := append([]hostbox.StopCgroupIdentity(nil), j.Cgroups...)
diff --git a/internal/host/claimstop_run.go b/internal/host/claimstop_run.go
index a576159c0..bc33340a5 100644
--- a/internal/host/claimstop_run.go
+++ b/internal/host/claimstop_run.go
@@ -82,8 +82,11 @@ func RunInstanceControl(args []string) {
log.Print(err)
}
finishStartup()
- execution.Start(ctx)
- handler.(ClaimRootHTTP).servingParks.Start(ctx)
+ lifecycle, ok := handler.(interface{ Start(context.Context) })
+ if !ok {
+ log.Fatal(ErrClaimStopUnavailable)
+ }
+ lifecycle.Start(ctx)
fenceDone := make(chan struct{})
go func() { defer close(fenceDone); runHostFencePass(ctx) }()
defer func() { cancel(); <-fenceDone }()
diff --git a/internal/host/rootexecution_recovery.go b/internal/host/rootexecution_recovery.go
index 4edc29977..3b87f649d 100644
--- a/internal/host/rootexecution_recovery.go
+++ b/internal/host/rootexecution_recovery.go
@@ -3,6 +3,7 @@ package host
import (
"context"
"errors"
+ "log"
"path/filepath"
"time"
@@ -42,6 +43,8 @@ func recoverRootExecution(ctx context.Context, dataDir, guardDir string) (result
return err
}
defer reg.Close()
+ // FS-R4: an enrolled census keeps retained root state under maintenance
+ // even if claims are now off; the no-census branch above stays separate.
ctx = context.WithValue(ctx, claimEntryKey{}, claimEntry{FlagRead: true, Enabled: true, Store: store})
ctx = withClaimRuntimeResources(ctx, ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: dataDir, OptDir: filepath.Dir(filepath.Dir(guardDir)), UnitsDir: "/etc/systemd/system"})
ctx, err = ensureProductionClaimAssembly(ctx)
@@ -69,8 +72,11 @@ func recoverRootExecution(ctx context.Context, dataDir, guardDir string) (result
return err
}
root := handler.(ClaimRootWithLaunch).Root
- if err := root.RecoverPending(ctx); err != nil {
+ if err := runtime.broker.Recover(ctx); err != nil {
return err
}
- return runtime.broker.Recover(ctx)
+ if err := root.RecoverPending(ctx); err != nil {
+ log.Print(err)
+ }
+ return nil
}
diff --git a/internal/hostbox/servinghandover.go b/internal/hostbox/servinghandover.go
index 8109e0ee5..14fac53eb 100644
--- a/internal/hostbox/servinghandover.go
+++ b/internal/hostbox/servinghandover.go
@@ -253,17 +253,20 @@ func (h ServingHandover) Recover(ctx context.Context) error {
if err != nil {
return err
}
+ var failures []error
for _, e := range entries {
if e.IsDir() || filepath.Ext(e.Name()) != ".json" {
continue
}
body, err := readTrustedJournal(filepath.Join(h.JournalDir, e.Name()))
if err != nil {
- return err
+ failures = append(failures, err)
+ continue
}
var j coldJournal
if decodeStrict(body, &j) != nil || j.V != 1 || j.ID+".json" != e.Name() || j.ServingInstallation == nil {
- return ErrTypedJournal
+ failures = append(failures, ErrTypedJournal)
+ continue
}
if j.Phase == "complete" {
found := false
@@ -275,10 +278,11 @@ func (h ServingHandover) Recover(ctx context.Context) error {
}
}
if err = h.Run(ctx, j.Prior, j.Successor); err != nil {
- return err
+ failures = append(failures, err)
+ continue
}
}
- return nil
+ return errors.Join(failures...)
}
func (h ServingHandover) at(point string) error {
if h.checkpoint != nil {
diff --git a/internal/hostbox/servinghandover_linux_test.go b/internal/hostbox/servinghandover_linux_test.go
index d5fecd0e0..51df3f7ca 100644
--- a/internal/hostbox/servinghandover_linux_test.go
+++ b/internal/hostbox/servinghandover_linux_test.go
@@ -165,3 +165,30 @@ func TestServingHandoverRefusesOldDestructiveRecovery(t *testing.T) {
t.Fatal(err)
}
}
+
+func TestServingHandoverStaleJournalDoesNotBlockHealthy(t *testing.T) {
+ h, p, n := servingHandoverFixture(t)
+ h.checkpoint = func(point string) error {
+ if point == "intent" {
+ return errors.New("interrupt")
+ }
+ return nil
+ }
+ if h.Run(t.Context(), p, n) == nil {
+ t.Fatal("no interrupted intent")
+ }
+ stale := filepath.Join(h.JournalDir, "000-stale.json")
+ if err := os.WriteFile(stale, []byte("stale journal"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ h.checkpoint = nil
+ if h.Recover(t.Context()) == nil {
+ t.Fatal("stale journal was not reported")
+ }
+ if b, err := os.ReadFile(stale); err != nil || string(b) != "stale journal" {
+ t.Fatal("stale evidence changed", err)
+ }
+ if err := h.Guard.With(t.Context(), p.Host, hostguard.Captured{Incarnation: n.Incarnation, Stamp: n.Stamp}, func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil {
+ t.Fatal("healthy journal not recovered", err)
+ }
+}
diff --git a/internal/provision/plan14_legacy_identity_test.go b/internal/provision/plan14_legacy_identity_test.go
index 0dd77dfa2..1cfe45139 100644
--- a/internal/provision/plan14_legacy_identity_test.go
+++ b/internal/provision/plan14_legacy_identity_test.go
@@ -616,12 +616,21 @@ func TestPlan14SeedOverlayPreservesLegacySeal(t *testing.T) {
if err := loadRootReadInventory(root+"/internal/provision/testdata/plan14-root-reads-seed.tsv", &inv, true); err != nil {
t.Fatal(err)
}
- if len(inv.Legacy) != before {
- t.Fatal("seed changed the legacy fingerprints")
+ if len(inv.Legacy) != before+3 {
+ t.Fatal("FS-R6 permits exactly three additional legacy fingerprints")
}
- if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 {
+ if errs := checkRootReadLegacy(inv, "b5b04f416089d97dbff3b9c6b6cb9d9c83f874449577946f651dc8c7a66a9f67"); len(errs) != 0 {
t.Fatal(errs)
}
+ // FS-R6 keeps these existing legacy classes; the seed cannot promote them.
+ for _, key := range []string{"internal/activator/activator.go:Service.sweepOnce", "internal/activator/servingretry.go:ServingRetries.Wrap", "internal/host/relocationflush.go:ReleaseFlushFiles.Flush"} {
+ if e := inv.Entries[key]; e.Class != "legacy" || e.Task != "accepted-legacy" || !strings.Contains(e.Basis, "census-ruling-FS-R6") {
+ t.Fatalf("unruled legacy disposition %s: %+v", key, e)
+ }
+ }
+ if e := inv.Entries["internal/host/claimwakeassembly.go:ClaimWakeAssembly.Build"]; e.Task != "C,G,J1,J4" {
+ t.Fatal("wake assembly lost its owners", e.Task)
+ }
for _, key := range []string{
"internal/host/claimconstruction.go:OpenProductionClaimAssembly",
"internal/host/claimproduction.go:ProductionClaimOwnerProvider.OpenClaimOwners",
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 37f398285..4706c311b 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -747,6 +747,8 @@ func TestPlan14RootReadCensus(t *testing.T) {
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-seed.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
+ // FS-R6 explicitly preserves these three legacy readers; all other proposed rows remain unapproved.
+ seal = "b5b04f416089d97dbff3b9c6b6cb9d9c83f874449577946f651dc8c7a66a9f67"
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index de7ec3c7c..51c29f1e7 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,7 +1,16 @@
-# Files seed overlay; loads LAST. Run 24 proposals, not census approval.
+# Files seed overlay loads LAST. Run 25; FS-R6 legacy dispositions explicitly ruled; other rows remain proposals.
+@legacy c4009ab5fb7d91ab65fde5cda910d796b2a98109831c7e90e1bef18c1a77ff48 internal/activator/activator.go Service.sweepOnce legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; diagnostic throttling only; registry remains legacy registry:Hibernate@12209,registry:Hibernate@21380,registry:Hibernate@5124,registry:Hibernate@6794,registry:Hibernate@9200,registry:Host@21449,registry:Host@21507,registry:Host@21879,registry:Host@21972,registry:Host@22707,registry:Host@22789,registry:Host@23854,registry:Host@24033,registry:Host@24082,registry:NextDue@22662,registry:Status@21396,whole:Route@12211,whole:Route@14144,whole:Route@15854,whole:Route@16044,whole:Route@16337,whole:Route@16509,whole:Route@16656,whole:Route@20867,whole:Route@22376,whole:Route@22716,whole:Route@22723,whole:Route@23669,whole:Route@24066,whole:Route@5126,whole:Route@6796,whole:Route@9202 74872baf2607ef6e24d8466dca33aa9f449b45c712377d3474f7d5c87368f62f
+@legacy fae097bc67df1153c4fe96526cdbbdc0800fe9f989edae9c34e243962fb5c19d internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; BeforeReserve refuses only; cleanup deadline bounds lifetime; route identity remains legacy file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@5991,file:Read@6656,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6522,whole:Route@876,whole:Route@954 e0fe1c8acbe4b1631ff9c3ce63dceba8297fbeab55fc6e4ace72be1d2d9d5bfa
+@legacy e84357c7d4acebbf3a83055c10796752ad67b32f98248f54361a387d2682d7d6 internal/host/relocationflush.go ReleaseFlushFiles.Flush legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; tenant files and env are published data, never authority file:Open@772 7cc12267c16d9d1ae1116c9d10528f97d3983c77fb6486e0aac73142d8999c4b
internal/activator/activator.go Service.serveWakeError 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 registry:Host@1353,registry:Host@1417 377f6ed44f225f9aa6bba4ebfb4350cd7dcf9c01dd3f932522b35a7df6b29032
-internal/activator/activator.go Service.sweepOnce authority B,C,D,F,J4 rev3=PROPOSED-seed; existing activator lifecycle and due-wake state plus owned seed exclusion and incarnation-keyed diagnostics; normal refusal and preparation ordering remain authoritative registry:Hibernate@12209,registry:Hibernate@21380,registry:Hibernate@5124,registry:Hibernate@6794,registry:Hibernate@9200,registry:Host@21449,registry:Host@21507,registry:Host@21879,registry:Host@21972,registry:Host@22707,registry:Host@22789,registry:Host@23854,registry:Host@24033,registry:Host@24082,registry:NextDue@22662,registry:Status@21396,whole:Route@12211,whole:Route@14144,whole:Route@15854,whole:Route@16044,whole:Route@16337,whole:Route@16509,whole:Route@16656,whole:Route@20867,whole:Route@22376,whole:Route@22716,whole:Route@22723,whole:Route@23669,whole:Route@24066,whole:Route@5126,whole:Route@6796,whole:Route@9202 74872baf2607ef6e24d8466dca33aa9f449b45c712377d3474f7d5c87368f62f
+internal/activator/activator.go Service.sweepOnce legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; diagnostic throttling only; registry remains legacy registry:Hibernate@12209,registry:Hibernate@21380,registry:Hibernate@5124,registry:Hibernate@6794,registry:Hibernate@9200,registry:Host@21449,registry:Host@21507,registry:Host@21879,registry:Host@21972,registry:Host@22707,registry:Host@22789,registry:Host@23854,registry:Host@24033,registry:Host@24082,registry:NextDue@22662,registry:Status@21396,whole:Route@12211,whole:Route@14144,whole:Route@15854,whole:Route@16044,whole:Route@16337,whole:Route@16509,whole:Route@16656,whole:Route@20867,whole:Route@22376,whole:Route@22716,whole:Route@22723,whole:Route@23669,whole:Route@24066,whole:Route@5126,whole:Route@6796,whole:Route@9202 74872baf2607ef6e24d8466dca33aa9f449b45c712377d3474f7d5c87368f62f
internal/activator/activator.go type Service authority B,C,D,F,J4 rev3=PROPOSED-seed; existing activator lifecycle and due-wake state plus owned seed exclusion and incarnation-keyed diagnostics; normal refusal and preparation ordering remain authoritative ad71a6a968693f86285e2d61f8a65df19251761e3b7694056fc829767e51c9ac
+internal/activator/cleanup.go CleanupContext authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join cf999f42acf01f4c68998f66da84ac40df4026fc5d1337958f703129f4cba01d
+internal/activator/cleanup.go WithCleanupBudget authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join 01b9fcc13496122c211b9f5520a73b42b8b16b81a0a0f9c3174774c269ca8e22
+internal/activator/cleanup.go cleanupBudget.start authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join 7131591c80ae84772fc33e5cd933ef938cf38ea85b6d1b40a987eabc4ee8c69b
+internal/activator/cleanup.go type cleanupBudget authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join 3dd761b939faa3a5f26a8b5a099bdbc4761d642e77202aeae1d2fd484afbdc89
+internal/activator/cleanup.go type cleanupBudgetKey authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join 62209a88c68983d0d6dd17667c2f1d07ec3f54f1add299191bdfcc6953333402
+internal/activator/cleanup.go type cleanupContextKey authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join d568563d33ac11e33615e2a294deacbaa726321529d49fc71fde8f63ab204c4f
internal/activator/maintenance.go Service.LiftFence authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release e3b978890731f441369476da9151e9b6cddc17627bea370548eb686c5ef7494f
internal/activator/maintenance.go Service.LiftFenceAs authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release 260411d3ccb3648990fe43be308e7caccd54a43b89a0e9adce69add5172369dd
internal/activator/maintenance.go Service.fenceAndStop authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release 770b4a033e91e154549e86c03683ca6512d0de3025d6d354f6bb86dfc864b2c4
@@ -16,35 +25,38 @@ internal/activator/seed_exclusion.go SeedExclusion.Release authority B,C,D,F,J4
internal/activator/seed_exclusion.go Service.AcquireSeedExclusion authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release cd98d58320a590564a599bd379cf146b3ed1499032089bbfac3ddde2424619aa
internal/activator/seed_exclusion.go type SeedExclusion authority B,C,D,F,J4 rev3=PROPOSED-seed; shared activator state and opaque owner token; waits existing request, scheduled-token and stop preparations before fencing; exact owner alone may resume or release 9259b90bd5130f5abd76c46ffc395b89820e26c3bd73461128d82f4fedca96d6
internal/activator/servingretry.go ServingRetries.SettleAbandoned authority C,J4 rev3=PROPOSED-seed; current serving retry identity, stop settlement and bounded reservation; capacity checked before side effects and immediately before reservation; explicit rerun settlement spends no new attempt file:Exec@279,file:Read@387,file:Read@732 8885d486d4daea05411f8c5ab0afa5b340542bb624bbfe82518d0b1f5063d171
-internal/activator/servingretry.go ServingRetries.Wrap authority C,J4 rev3=PROPOSED-seed; current serving retry identity, stop settlement and bounded reservation; capacity checked before side effects and immediately before reservation; explicit rerun settlement spends no new attempt file:Prepare@128,file:Prepare@4369,file:Prepare@4499,file:Read@2932,file:Read@3575,file:Read@5098,file:Read@6063,file:Read@6728,whole:Route@1212,whole:Route@1572,whole:Route@1670,whole:Route@1970,whole:Route@2382,whole:Route@4517,whole:Route@5465,whole:Route@6594,whole:Route@876,whole:Route@954 dc27bad808ca7cab72e3e0b6ea229186f954f982db44e5d6da3d0fcad1ac6942
+internal/activator/servingretry.go ServingRetries.Wrap legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; BeforeReserve refuses only; cleanup deadline bounds lifetime; route identity remains legacy file:Prepare@128,file:Prepare@4325,file:Prepare@4455,file:Read@2888,file:Read@3531,file:Read@5054,file:Read@5991,file:Read@6656,whole:Route@1212,whole:Route@1550,whole:Route@1648,whole:Route@1948,whole:Route@2360,whole:Route@4473,whole:Route@5421,whole:Route@6522,whole:Route@876,whole:Route@954 e0fe1c8acbe4b1631ff9c3ce63dceba8297fbeab55fc6e4ace72be1d2d9d5bfa
internal/activator/swaphold.go Service.wakeFailureEvent 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 debf64b1e73210eb4a6c88dd82f76e8be2e3e0a4f43e272341efe227703f9ca0
+internal/host/agent.go runAgent authority B,C,D,F,J4 rev3=PROPOSED-seed; agent startup and claim-gated seed endpoint; shared shutdown deadline and handler join before shared closure file:Exec@13674,file:Open@731,registry:App@10065,whole:Route@10057,whole:Route@22981 95e74886a35fbe7d522c47e94b531df15e8a1bacc5417f5540bbff04d9039b4e
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; 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 file:Exec@476,file:Exec@661,file:Open@1102,file:Read@2129,file:Read@3805,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@4588,whole:Route@4683,whole:Route@767 62519bbce3a714081c742820e2a0824faa3b56f282c0a81da34870c29b9116fd
+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.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
internal/host/claimfilesseed.go type filesSeedIncompleteError 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 b4ad686b56fceed86a7bab5119a7b3b7eb2fd45530a2603880686ae951cc3cf0
internal/host/claimfilesseed.go type filesSeedPending 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 ba35a4a7eb695b04e1cb2319719179021e05ac7c54b62ce773833228c29318df
-internal/host/claimfilesseed_command.go RunSystemFiles 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 2c67584a8d1453449596e0e882bb37e3b8474e3f155b5353b3d8247458dec364
+internal/host/claimfilesseed_command.go RunSystemFiles authority B,C,D,F,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join c73d8c53e1ef7fbdc8a28c239994b0d61b31c293e8be8bc9d962f50d25abfd90
internal/host/claimfilesseed_command.go filesSeedExitCode 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 e5d08e5fed16cf291b169de7dc0cdba359e745042c61bcde1b6f3973fb64016b
internal/host/claimfilesseed_command.go parseFilesSeed 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 f5629089a0025fcc866d539c6c3769f40d289cb3c37915e97262856ff1e2c285
-internal/host/claimfilesseed_command.go runFilesSeedOneShot 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 a05aa93c6c58d091c3cec4af88f4d5533b5e6874b2933b9df7bb9de99d326b98
-internal/host/claimfilesseed_command.go runSystemFiles 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 2ed024effca69be211670af2e160365b9cf2e2029986de6f63dc8e29715bf4fa
+internal/host/claimfilesseed_command.go runFilesSeedOneShot authority B,C,D,F,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join d0ab732d5575dc26c22dc04548332bb81eb08017d32a438da0a96c4a012ab776
+internal/host/claimfilesseed_command.go runSystemFiles authority B,C,D,F,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join 3e7f6a76df209e8cd0a34b085d52f71d90cc77f84d8d6b158761b5b0efce9fa3
internal/host/claimfilesseed_command.go var errFilesSeedDelegation 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 d992c13d29ae3fd9ee114979aec3ee69302b62e4c8b1f75cdc798bba3fb31add
internal/host/claimfilesseed_delegation_linux.go filesSeedEdgePID authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses file:CommandContext@141 a722d32c26a6a991d6f55aaf35b27b53005296f2529e5fbbb620cefef5478a58
internal/host/claimfilesseed_delegation_linux.go filesSeedManager authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses file:ReadFile@176 2bd4e8d2f5fd72b5a8eaa6415f32b9d92bfd6dd2d96df4c6ad3e5c93f2f1bd53
+internal/host/claimfilesseed_delegation_linux.go filesSeedPeerObservation authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery file:ReadFile@609,file:ReadFile@935 1251e5d5ca4cddf1fa63c0fddd65d8d0be8c69bdb02157372ebac96b241b5175
internal/host/claimfilesseed_delegation_linux.go verifyFilesSeedDelegation authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses file:Fstat@1025,file:Open@857,file:ReadFile@1210,file:ReadFile@1543 dbdc836a7b54da126de53dd375d0fa6541fbd0973e0ba705bf16bc88bbb1f874
internal/host/claimfilesseed_delegation_other.go filesSeedEdgePID authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses 074a97695b0ce76343f9d7cb632b9c54cd69568ec38b70507e15b5ec61a2d33b
internal/host/claimfilesseed_delegation_other.go filesSeedManager authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses 140c41b41bf95ab631c3aea59c444fce7aa8408cd7e7469655c11f714098c469
internal/host/claimfilesseed_delegation_other.go verifyFilesSeedDelegation authority C,J4 rev3=PROPOSED-seed; installed edge MainPID, stable controller identity and kernel cgroup identity, owner and delegated controls; root rejects non-controller and unprepared delegation; non-Linux refuses b3c849c2a61ed819d5392a03ad88b030653c772f036567109e5f634c336a8ebb
internal/host/claimfilesseed_guard.go movesFiles.checkServingFilesEvidence authority C,J4 rev3=PROPOSED-seed; safe nested layout and bounded manifest metadata; evidence absence refuses before retries; only genuine empty authority can omit instance directory whole:Route@265 4c75f947c4061022996dca4af4a54a10ad974e5a7b1fbe69c3f628d5e5decf84
internal/host/claimfilesseed_guard.go servingFilesPrerequisites authority C,J4 rev3=PROPOSED-seed; bounded current-version file evidence and exact claim reread before retry wrappers; typed missing-evidence refusal carries incarnation for rate-limited diagnostics file:Read@761,whole:Route@548 52abed55f7182a0f3166b9dcea1097c49e68f17bd83a13d924bee9d7a1ec5ea7
+internal/host/claimfilesseed_killed_linux.go LinuxStopKernel.recoverUnlaunchedAbsent authority C,D,J4 rev3=PROPOSED-seed; root recovery and lifetime; per-host failures retained and reported, controller recovery first, pressure cleanup independently budgeted file:Lstat@1566,file:Read@1222,file:Read@590,file:Read@740 fbd4fa26890d04e8a5f34caebd10b1ea171374ccf677a20c63ad6d09057042b7
internal/host/claimfilesseed_retry.go filesSeedOperation.prerequisites authority C,J4 rev3=PROPOSED-seed; exact owned host exclusion, serving claim and nested layout; bypass only missing evidence for one eligible private seed; rerun settlement uses ordinary root stop and spends no new attempt registry:Host@256,whole:Route@596,whole:Route@798 44a9d61481d9499b1e1305595a43d94cd364da6abe1f91c337f4deb2c48d2fbc
internal/host/claimfilesseed_retry.go settleFilesSeedRerun authority C,J4 rev3=PROPOSED-seed; exact owned host exclusion, serving claim and nested layout; bypass only missing evidence for one eligible private seed; rerun settlement uses ordinary root stop and spends no new attempt registry:Host@241,registry:Host@323,registry:Host@721,whole:Route@485 24337d0b6a864850a4f4203757ed40d634a770abff9dc4c96cd9c746ba8ced2e
internal/host/claimfilesseed_runtime.go newFilesSeedRuntime authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt whole:Route@1955 3d91da897f65e34f09d636bac9a19b25d641df17d10b37c69a724cf122b9d27d
-internal/host/claimfilesseed_runtime.go prepareFilesSeed authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt file:Prepare@1717,file:Prepare@400,file:Read@321,file:Read@955,registry:Host@2145,registry:Host@243,whole:Route@1494,whole:Route@1607,whole:Route@1738 3022ec3df58ccb809b6b68b67dd0b2a0b4ad3252168bcf98ad2c9a2e303fab3f
+internal/host/claimfilesseed_runtime.go prepareFilesSeed authority C,J4 rev3=PROPOSED-seed; bounded request lifetime, shared cleanup deadline and exclusion; no resource closure before handler join file:Prepare@1699,file:Prepare@400,file:Read@321,file:Read@937,registry:Host@2127,registry:Host@243,whole:Route@1476,whole:Route@1589,whole:Route@1720 47c7e84c14565eae81304c125ddcd30680c5e06aaaa29d0c7c571effde19cbb7
internal/host/claimfilesseed_runtime.go type filesSeedRuntimeFactory authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt 950b8bdf6e212ca9a3ad996a5851215fe61302bb045ac6d47570d9b5867ef5a1
internal/host/claimfilesseed_select.go checkFilesSeedLayout authority C,J4 rev3=PROPOSED-seed; fresh claim and route, safe nested layout, complete local retained-admission chain and exact manifest versions; all entries checked for restore cap and real origin; missing source is never empty registry:DBPath@174,registry:DBPath@301,registry:Host@221,registry:Host@292,registry:Host@414,registry:Host@438,registry:Host@593 cb0de1645ceeb6726ca342bec8487a807b0da6f200a277c41be87e3bc62de4cd
internal/host/claimfilesseed_select.go filesSeedCandidates authority C,J4 rev3=PROPOSED-seed; claim keys and current executor before registry access, then exact ordinary release/config/baseline inputs; reject missing, foreign or unsupported selection file:Exec@1002,file:Read@313,file:Read@891 bde287a3a8f164607ba1ead5e788807ef1e2352215ebddaabed64243597574e7
@@ -60,16 +72,34 @@ internal/host/claimfilesseed_stage.go filesSeedOperation.stage authority C,J4 re
internal/host/claimfilesseed_stage.go newFilesSeedProvider authority C,J4 rev3=PROPOSED-seed; fresh private provider, real admitted tuple and origins; guarded safe source descriptor and wake snapshot; create conflicts compare bounded bytes at exact current version before commit file:Open@364 391a4dd6ae1ee7b12c49d4cf2cbb4007716634af75591565dc4c3f754d4cbf35
internal/host/claimfilesseed_stage.go type filesSeedCheckingStore authority C,J4 rev3=PROPOSED-seed; fresh private provider, real admitted tuple and origins; guarded safe source descriptor and wake snapshot; create conflicts compare bounded bytes at exact current version before commit 41f415ce4f78804c1191a2dfc450e355f44b979f9cdeb210a482c35b42591f4f
internal/host/claimfilesseed_stage.go type filesSeedOperation authority C,J4 rev3=PROPOSED-seed; fresh private provider, real admitted tuple and origins; guarded safe source descriptor and wake snapshot; create conflicts compare bounded bytes at exact current version before commit a34058915d17d03fd83847b5ebdaf38104ee763a3629b09e236ad30e50781046
+internal/host/claimfilesseed_transport_linux.go const filesSeedSocket authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery a87b27768dfe8a1e52bd69a384d36c4ffa91039a3c365cfe79a37cc7c22dd1c6
+internal/host/claimfilesseed_transport_linux.go filesSeedClient authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery file:Lstat@118,file:Lstat@715 c795363fa2fbb10cc5110e1c7d280d44033de523dbd18f7d17a3c8e662836594
+internal/host/claimfilesseed_transport_linux.go filesSeedServer.close authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 5c865a9041dd465a19f5cbb46047d5d416921316f07c009f3e0c5b206379558a
+internal/host/claimfilesseed_transport_linux.go filesSeedStream.Write authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 5d364cad4269accbc72f0be08820ad464c1ca0595d03b7f44d0afd6bd78cf708
+internal/host/claimfilesseed_transport_linux.go removeStaleFilesSeedSocket authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery file:Lstat@106,file:Lstat@593 5c8b1f5a9b4a1faa6a8f3d86e566c5ba32923e9a165af68d8ef2db699692bd18
+internal/host/claimfilesseed_transport_linux.go serveFilesSeed authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery eea07a5e4aaee1fb9459485651067bcd9a307a6a55f704d7e3e2b86f71132221
+internal/host/claimfilesseed_transport_linux.go startAgentFilesSeed authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 2cd42d699920d5b0075746824fed41c7bd679d029b87b5d5f7a7f24f790986b7
+internal/host/claimfilesseed_transport_linux.go startFilesSeedSocket authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery file:Lstat@428,file:Lstat@527 cc84a3d4a4b2c1c9616957fd128c129426fbf570142e3fe4c70347393748bd0b
+internal/host/claimfilesseed_transport_linux.go type filesSeedReply authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery d6eaf67e486cb06bdf42526fe236c5c43da9bad71a97f3f05f19d943e00a415b
+internal/host/claimfilesseed_transport_linux.go type filesSeedRequest authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 9be6d7a745e27a89cb6faabb28899578bef1602ba8b8dcf7f39efd97274eb42c
+internal/host/claimfilesseed_transport_linux.go type filesSeedServer authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 8a899cbc4d18bf65e2c53ee68d36fd05412c99bfc9d621b5e8ee7dd530124bbd
+internal/host/claimfilesseed_transport_linux.go type filesSeedStream authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery d51d90e4e9cd1ae397db0941798f719897c26c4e6ed84b289543eba363fe0a96
+internal/host/claimfilesseed_transport_other.go filesSeedClient authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 6e57a07086521dd818f025dfdbc964695ea10c32a5ad6e9d98498ab0032c0090
+internal/host/claimfilesseed_transport_other.go filesSeedServer.close authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 9340754f2e5e90a99a8b15d5cec4c8d27a070252e55e8664c323ecc658ad02d9
+internal/host/claimfilesseed_transport_other.go startAgentFilesSeed authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 71ef4ddedbcc4d35bb79ce23b53f21ad6801d7cff9814b6ccdefbd5816164594
+internal/host/claimfilesseed_transport_other.go type filesSeedServer authority C,J4 rev3=PROPOSED-seed; Unix peer credentials, pidfd liveness, repeated process/controller/cgroup identity, trusted socket; root lifecycle and bounded recovery 258bd08914bac2d2014a35d4c0a1c9e41f5299cb6ef5fd1532f2d266924c07de
internal/host/claimhandover_transport.go ClaimRootHTTP.serveColdHandover authority C,J4 rev3=PROPOSED-seed; authenticated existing handover transport; serving-specific transfer and repeated peer identity check under the host lock; operation handover remains separate 9ea7b9ea4a4962ad92a5f6ce19a24fcffca73e40aae9318a2d8c79ef25351e6a
internal/host/claimhandover_transport.go coldAdmissionHook authority C,J4 rev3=PROPOSED-seed; serving predecessor receipt or session-zero identity selector; root supplies the retained no-process result 70974148b3f47e4cb2057e4000c472cc4b372707fb1ff64b5e62582dc92b70c2
internal/host/claimjournal_transport.go ClaimRootHTTP.serveJournal authority C,J4 rev3=PROPOSED-seed; typed journal envelope; routes v4 serving-park selectors before generic journal replay; existing operations retain their dispatch file:Read@3311,whole:Route@4465 51276984eeb10a9480b9ff1a92ed8f16e9a141134cb3913ec75ff18c03570868
+internal/host/claimlaunch_production.go ClaimRootWithLaunch.Start authority C,J4 rev3=PROPOSED-seed; root recovery and lifetime; per-host failures retained and reported, controller recovery first, pressure cleanup independently budgeted 01a2a00a414aca8c5e0c9bc7b143b0c4690b242d2bc0530e75e8fe709f5c9a01
internal/host/claimlaunch_production.go claimLaunchFor authority C,D,J4 rev3=PROPOSED-seed; authenticated capability peer and production root delegation verifier; capability validates only and creates no writer or launch data-literal@414,file:CommandContext@605 b6c4f60ed5783496e89c45c9c612cbd8758b6a05c19a706c5eb5640dfea2505d
internal/host/claimlaunch_root.go ClaimLaunchRoot.Serve authority C,J4 rev3=PROPOSED-seed; authenticated capability peer and production root delegation verifier; capability validates only and creates no writer or launch ed7766ea6321a0a91e7be6a131363f05d06d3af55928ee3175f5b91f343f4868
internal/host/claimlaunch_root.go type ClaimLaunchRoot authority C,J4 rev3=PROPOSED-seed; authenticated capability peer and production root delegation verifier; capability validates only and creates no writer or launch f6ba324d4632eb0abed1c97b16e29ed83b165533bfcd82d91e70273d665a7adc
internal/host/claimowners.go checkClaimServingSources authority C rev3=PROPOSED-seed; in-process serving launch, marker and files provider availability before serving selection ed3ad706b8142cd514c04e7e6eeb37a61f881a0e160ffbcb78898d0f1d419351
internal/host/claimowners.go claimServingOwners.serving authority C,G,J4 rev3=PROPOSED-seed; claim executor and serving shape plus complete serving sources; missing launch, marker or files source refuses file:Exec@450 6d15b7a206526a4df95b4eb2cc5ac5973a7360947ef3eca7a1df8f3e04719719
internal/host/claimproduction.go ProductionClaimOwnerProvider.OpenClaimOwners authority C,G rev3=PROPOSED-seed; verified roster, cloud identity, floors and configured provider dependencies through existing helpers; no moves-provider Open or Close with moves disabled f1e9aa15fb6142f839bb76dafa574cad7122d6053cf2099d852ad97a528f1127
-internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C,J4 rev3=PROPOSED-seed; startup serving handover recovery before ordinary cold and root recovery; old destructive serving intents refuse 627b40daecf11e1a6a6c9f92aeb3ebcf226131da6dfa584ed85559c8b9b5ff30
+internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated cfbfeff484b45ace11a3465cf0628ddcc6c4353112071dd602b321f46bc565bf
+internal/host/claimretire_transport.go ClaimRootHTTP.Start authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated adb9230b314bf8bd173513df73b705be64a0ae93db67ea4784d881a061825e7d
internal/host/claimservingartifact_linux.go checkServingArtifactAbsence authority C,J4 rev3=PROPOSED-seed; source directory opened without symlinks; descriptor statx mount/device/inode and no-follow lookup of database, marker, WAL and SHM file:Fstatat@680,file:Statx@234 ab45d3f0651d25e8a6d1c48def6cc222c35dc152aca477ebd1555a67c9b89881
internal/host/claimservingartifact_other.go checkServingArtifactAbsence authority C,J4 rev3=PROPOSED-seed; non-Linux refusal; no source inspection or restore permission 27a7173119b5af75be262e9fcc56fddadb84a539f69fe5882f8c6613cfbc88c9
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
@@ -108,11 +138,12 @@ internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority
internal/host/claimservinglegacyroot_linux.go servingParkRoot.parkLegacyServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh serving claim, root installation, consumed legacy identity and exact stop/empty evidence; collected consumption refuses recapture, while consumption saved before the first intent still resumes file:Read@3525 c192eda0fc53bc5211b02123ed7745e8e2b1b2fc53ad5fb3b0d11fe95fd9672c
internal/host/claimservinglegacyroot_linux.go servingParkRoot.retainLegacyNoProcess authority C,J4 rev3=PROPOSED-seed; root coordinator result before publication; namespace and host guards, census, roster, fresh original claim and immutable retained stop; repeated preadmission witnesses tolerate only Gen, ETag and retry metadata changes file:Read@1210 5460530ae4367720515089e9fda3105aa1032b8282f4923bbf7f8af9600f4f63
internal/host/claimservinglegacyroot_linux.go servingParkRoot.validateLegacyRootEvidence authority C,J4 rev3=PROPOSED-seed; retained no-process or stopped-admission evidence; exact predecessor fields with canonical recorded directory revision bounded by the current actor aa0dfe36c042ab65fd826e4b83eb81bfb9d03e6a54daafa828d5164492329d0d
-internal/host/claimservingmaintenance.go servingParkLedger.maintainParks authority C,J4 rev3=PROPOSED-seed; private root retirement, slot and reboot-binding records under namespace exclusion; remove preparations before retirement and orphan bindings only after proven retirement absence; never delete artifacts through this sweep 9ae45d7c5896a5c673489dd34ed94510aaf7281b8f73c7f5a831346f0f2ee586
-internal/host/claimservingmaintenance.go servingParkRuntime.Start authority C,J4 rev3=PROPOSED-seed; root park records, slot ownership and deadlines; bounded candidate set, oldest complete parks, verified deletion under pressure or expiry; cancelled on shutdown fd8fb4702b9dcdf016343689e40e050006ec90d1af128cde283c388b79da458d
+internal/host/claimservingmaintenance.go maintainServingParkPass authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 5c038c33394b21f73af912c206e3249b262d093c463a090e7311c40cc7227be8
+internal/host/claimservingmaintenance.go servingParkLedger.maintainParks authority C,J4 rev3=PROPOSED-seed; census-ruling-FS-R6; identity-verified deletion of complete retained parks on expiry or pressure, oldest first 9ae45d7c5896a5c673489dd34ed94510aaf7281b8f73c7f5a831346f0f2ee586
+internal/host/claimservingmaintenance.go servingParkRuntime.Start authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 3b5b9f6192e2ffddbb214d1cfa78a0afbc1087d21abcd48aae1a407b31bd15b2
internal/host/claimservingmaintenance_linux.go claimServingRoom authority C,J4 rev3=PROPOSED-seed; descriptor-opened data filesystem capacity, conservative DiskFloor; root recovery and bounded cleanup; carlos capacity path has no root-only state dependency ce0cb25c53adfd3dba7344a98049f44fc3f0b3e633b22ad37299dea81503e403
internal/host/claimservingmaintenance_linux.go servingParkFS.room authority C,J4 rev3=PROPOSED-seed; descriptor-opened data filesystem capacity, conservative DiskFloor; root recovery and bounded cleanup; carlos capacity path has no root-only state dependency 5725a03b76664476dda6c2cb380339c26eff9967a4c04cf4e57fb9c68727872b
-internal/host/claimservingmaintenance_linux.go servingParkRoot.maintainServingParks authority C,J4 rev3=PROPOSED-seed; descriptor-opened data filesystem capacity, conservative DiskFloor; root recovery and bounded cleanup; carlos capacity path has no root-only state dependency 0c611463f1aa7c6a75348e460621305d54da6b619bf34394d430ba525d9608b7
+internal/host/claimservingmaintenance_linux.go servingParkRoot.maintainServingParks authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 97f6b94285efc9676038f012c9e1cb3b5454e3881d22f3cb8ac636cc0d6df69e
internal/host/claimservingmaintenance_linux.go servingRoomFile authority C,J4 rev3=PROPOSED-seed; descriptor-opened data filesystem capacity, conservative DiskFloor; root recovery and bounded cleanup; carlos capacity path has no root-only state dependency file:Fstatfs@78 9a76893f007a92f8a418660cc41fd9551d38b47523911ae1838bf110adb69d9a
internal/host/claimservingnamespace_linux.go servingParkFS.historicalNamespaceParent authority C,J4 rev3=PROPOSED-seed; private namespace records, topology and boot bindings; capacity refusal before new namespace capture file:Read@117 bdb9f4c2b649160fcb79eb4772f37242be2ebace16fd4c32009a943ce5b483b9
internal/host/claimservingnamespace_linux.go servingParkFS.namespaceParent authority A,C,J4 rev3=PROPOSED-seed; private namespace records, topology and boot bindings; capacity refusal before new namespace capture bae834ae17f4c3d45bceb9e7c3fd08785454a51825d72b0cf26a0f2c996c1f91
@@ -144,7 +175,7 @@ internal/host/claimservingpark_other.go newServingParkAuthority authority C,J4 r
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; source boot and persistent directory handle captured before mutation and rechecked after all artifact reads a5560b338356792386a5a1b2e27cb08ca0d5aa024cd03b567a78dfd986ce7871
internal/host/claimservingpark_other.go servingParkFS.CheckParked authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 3a8cb4d8052bbdea8cf7a300944de72c68012e6efa252c293fc1dc06a3a2150e
-internal/host/claimservingpark_other.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; recorded event and artifact identities; validates complete child set before journaled unlink, then removes only the recorded empty event with durable intent and progress 8c169e8428b913d2f05b502ce971be49d5febaad95d8ae811aae21c81f3434ce
+internal/host/claimservingpark_other.go servingParkFS.Delete authority C,J4 rev3=PROPOSED-seed; census-ruling-FS-R6; non-Linux refusal only; returns ErrUnsupported without deletion 8c169e8428b913d2f05b502ce971be49d5febaad95d8ae811aae21c81f3434ce
internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority 9d215b168da646166104db5624727038f793d2ae0c40eb71f23883f12b6b4b13
internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority; new intents record the private event filesystem handle before the first rename; old records gain no persistent evidence file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29
internal/host/claimservingpark_records.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-identical root stop sets including targets, evidence timestamps and original receipt bytes c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88
@@ -158,7 +189,6 @@ internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority
internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection; old-format replay preserves original consumption and source evidence without inferred handles file:Prepare@3772,file:Read@1774 c0cdd64d93a729abd8cf2a48606579516824c35181f977631de4a1a395a0b8c7
internal/host/claimservingpark_records.go servingParkLedger.read authority C,J4 rev3=PROPOSED-seed; root-private retirement bytes and separate mount projection; forward reads and inverse progress writes validate both representations f029f0fb8d31bdccaf86a5e336ffc1643eb2487fc7adb87852a9d21ad287e732
internal/host/claimservingpark_records.go servingParkLedger.readHistorical authority C,J4 rev3=PROPOSED-seed; root-private retirement bytes and separate mount projection; forward reads and inverse progress writes validate both representations file:Read@259 7af371e0f68db2dbf035c4dd75c9f8312ea3733d1bbfbdd4139088b2e1805bb3
-internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry aec4f50b45d1d955fac9814fe9e0d403f5f3f7f939c71f8f6b8ac15e9173b846
internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad
internal/host/claimservingpark_records.go servingParkLedger.save authority C,J4 rev3=PROPOSED-seed; root-private retirement bytes and separate mount projection; forward reads and inverse progress writes validate both representations 187f2c360d38e10a9f22b20f4c02a021c66b9308cad404e536ef5d0f0a860674
internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes; last-retirement identity supports discarded/artifact-free completion replay file:Read@179 239b806b2e2bfdd568f5d6b749d299f0bc7b118d675e872429432591a89b7b68
@@ -174,12 +204,12 @@ 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; service lifetime cancellation and join; bounded startup results rendered only with indexed host/record/fixed-reason text 0aa3ef7edd0ac2c12b4221907ccd5927e7e3e03a4c2c0b77b669bc10cc49c241
+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 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_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; startup validates raw persistent namespace and commits boot binding; incomplete records still require current installation and stop/launch authority 87c40006c0aeb45e507a81c715f658f83100c07002d8fc5c5d53a1d87e70c196
+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
internal/host/claimservingpark_transport.go ClaimRootHTTP.serveServingPark authority C,J4 rev3=PROPOSED-seed; authenticated edge connection and census actor; only identity selectors reach root authority; completed replay rechecks census and enrollment; pressure unavailable 6d5783a57cc3a7ee3c8303b9b205e5f5ccf0fd606f1e73e3bf4e69092f5085f4
internal/host/claimservingpark_transport.go newServingParkRuntime authority C,J4 rev3=PROPOSED-seed; private state and platform-specific root authority; constructs bounded service lifetime without starting workers c432ba5861dbb9b10bf102216cdeb5eb9b2cea21b547779a981a91a071d1dfc8
@@ -250,12 +280,13 @@ internal/host/claimservingtopology_linux.go type servingParkRebootObservation au
internal/host/claimservingtopology_linux.go type servingParkTopology authority C,J4 rev3=PROPOSED-seed; persistent parent/root evidence attached only when root creates and records a new namespace 16a7fce2850bf204ddd67aa10876979be08a5d500a21ea3072e2fc5c1c369586
internal/host/claimstop.go ClaimStopCoordinator.witnessNoProcess authority C,G,J4 rev3=PROPOSED-seed; existing peer, claim, root inventory and positive kernel no-process checks; local adopted holder requires new retained-evidence binding; original observation is retained before receipt publication file:Exec@1845,file:Read@2541,file:Read@668 4a0df51297d2f6ed2e9c284ee11c26b73747bf84e1da5214d00f190eece5eb38
internal/host/claimstop.go type ClaimStopCoordinator authority C,G,J4 rev3=PROPOSED-seed; root-only coordinator dependency for retaining its own session-zero observation; nil preserves existing refusal for adopted legacy holders b7114aa7340cdba82fef42d62b784eae16da7acc48407ae6edba6453a4889f9a
-internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-seed; instance-control lifecycle; start cancellable root park maintenance and join it before closing private state 6adf5d9e0bbc87b7e010fb445bf624290fe63b620fb8b42941724e8ad7407bd0
-internal/host/claimwakeassembly.go ClaimWakeAssembly.Build authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt file:Command@10308,file:Prepare@7029,file:Read@1348,file:Read@1442,registry:Backing@2672,registry:Host@12922,registry:Host@3354,registry:Host@3957,registry:Host@4002,registry:Host@4709,registry:Host@5430,registry:Host@6477,registry:Host@7185,registry:Host@7858,registry:Host@9166,registry:Seed@2655,registry:Tunnel@2727,registry:Unit@2692,whole:Route@10446,whole:Route@14642,whole:Route@14714,whole:Route@2899,whole:Route@5830 2613ac52b918f0b9a08551ceb05a8f0151ced0d22b10a449a96fae76af863d62
+internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 393fdc63baeed67092c572fb003e9ed224d1ce37c556060033dffdb4bfcedcce
+internal/host/claimwakeassembly.go ClaimWakeAssembly.Build authority C,G,J1,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt file:Command@10308,file:Prepare@7029,file:Read@1348,file:Read@1442,registry:Backing@2672,registry:Host@12922,registry:Host@3354,registry:Host@3957,registry:Host@4002,registry:Host@4709,registry:Host@5430,registry:Host@6477,registry:Host@7185,registry:Host@7858,registry:Host@9166,registry:Seed@2655,registry:Tunnel@2727,registry:Unit@2692,whole:Route@10446,whole:Route@14642,whole:Route@14714,whole:Route@2899,whole:Route@5830 2613ac52b918f0b9a08551ceb05a8f0151ced0d22b10a449a96fae76af863d62
internal/host/claimwakeassembly.go type ClaimWakeAssembly authority C,J4 rev3=PROPOSED-seed; fresh private serving owner and captured ordinary preparation/stop closures; reread same-writer claim after root certification before C8 origin proof; ambiguous cleanup retains exclusion and retry debt c13a15027ae22cf7a030b9cdedf13de4ae0f093f64b3e4a35c0614446964c94c
internal/host/claimwakestop.go ClaimAssembly.collectCapturedStop authority C,G,J4 rev3=PROPOSED-seed; fresh claim and verified local stop identity; session-zero legacy holder may name only the local writer box, with no admissions; root independently proves and retains no-process evidence file:Exec@2994,file:Read@605 d4cd80663dfae5f9737a74e319d3423f4ef853e1843666b02b4cd2341e869d72
internal/host/movesfiles.go movesFiles.Establish authority C,H3 rev3=PROPOSED-seed; ordinary origin proof and single C8 commit; private seed checks exact admitted writer and stages shared snapshot; no armed live provider 172fe8888da88c249b7990bd9fb4bd2821bd402fb96ab52af13c58500f31d50f
-internal/host/relocationflush.go ReleaseFlushFiles.Flush authority C,J4 rev3=PROPOSED-seed; existing release-flush authority and safe source descriptor; shared wake/files staging preserves single manifest commit and unstable-file refusal file:Open@772 7cc12267c16d9d1ae1116c9d10528f97d3983c77fb6486e0aac73142d8999c4b
+internal/host/relocationflush.go ReleaseFlushFiles.Flush legacy accepted-legacy accepted-legacy;rev3=PROPOSED-seed;basis=census-ruling-FS-R6; tenant files and env are published data, never authority file:Open@772 7cc12267c16d9d1ae1116c9d10528f97d3983c77fb6486e0aac73142d8999c4b
+internal/host/rootexecution_recovery.go recoverRootExecution authority D rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 44d102ddc175fb2df17832a64b34063cc17e6abf4fe8779dd3843344336dd5c0
internal/host/rootexecution_runtime.go claimExecutionRuntime.Close authority C,J4 rev3=PROPOSED-seed; existing execution monitor lifetime plus serving-park lifetime; both share caller deadline before authority stores close 08352d84ef3a8cd55924338abc3d33229f2216a608c0daef707aec8c472e2a8e
internal/hostbox/coldhandover.go ColdHandover.Run authority C,J4 rev3=PROPOSED-seed; production operation-only gate refuses destructive serving handover, including old pending journals; existing move effects unchanged file:Exec@1027,file:Lstat@9838,file:OpenRoot@9760,file:Read@1379,registry:Addr@3654,registry:Addrs@3672,registry:Backing@3691,registry:Canary@3748,registry:ClaimIncarnation@3532,registry:ClaimIncarnation@4034,registry:ClaimOwner@10588,registry:ClaimOwner@4116,registry:ClaimOwner@4136,registry:ClaimOwner@6011,registry:ClaimOwner@7439,registry:ClaimOwnership@10180,registry:ClaimOwnership@3502,registry:ClaimOwnership@4480,registry:ClaimOwnership@4698,registry:ClaimOwnership@4731,registry:ClaimRowID@10478,registry:ClaimRowID@10559,registry:ClaimRowID@4154,registry:ClaimRowID@4762,registry:ClaimRowID@7464,registry:ClaimStamp@3577,registry:ClaimStamp@4081,registry:Host@7401,registry:Kind@3610,registry:Name@4816,registry:Name@7420,registry:Platform@3732,registry:Registration@4180,registry:Registration@7489,registry:Replica@3762,registry:Route@10180,registry:Route@4480,registry:Route@4731,registry:Route@5262,registry:Route@6011,registry:Timers@4679,registry:Timers@4698,registry:Timers@4762,registry:Timers@4816,registry:Timers@7276,registry:Tunnel@3712,registry:Unit@3636,whole:ClaimOwnership@10194,whole:ClaimOwnership@3506,whole:ClaimOwnership@4492,whole:ClaimOwnership@4714,whole:ClaimOwnership@4743,whole:ColdRows@10182,whole:ColdRows@11294,whole:ColdRows@11304,whole:ColdRows@4977,whole:ColdRows@4985,whole:ColdRows@4993,whole:ColdRows@5000,whole:ColdRows@5264,whole:ColdRows@6013,whole:ColdRows@7278,whole:Route@10188,whole:Route@3487,whole:Route@3847,whole:Route@4279,whole:Route@4287,whole:Route@4338,whole:Route@4346,whole:Route@4486,whole:Route@4737,whole:Route@5271,whole:Route@6020,whole:Timer@4018,whole:Timer@4292,whole:Timer@4300,whole:Timer@4351,whole:Timer@4389,whole:Timer@4685,whole:Timer@4704,whole:Timer@4768,whole:Timer@4822,whole:Timer@7285 050ae1e6818687e19e7a0cd443c09e837e54bdc00f4ea1feb6430cfedd2b5343
internal/hostbox/coldhandover.go type ColdHandover authority C,J4 rev3=PROPOSED-seed; operation-only production cold-handover policy; no source-preserving serving authority from operation cleanup 874b2b74ea349dd93655d4e11f4d9e97752c3f216593ebb922d819caba7981c4
@@ -267,7 +298,7 @@ internal/hostbox/journalservice.go validateJournalRequest authority C,J4 rev3=PR
internal/hostbox/restore.go ColdRestore.Install authority C,J2,J4 rev3=PROPOSED-seed; selected database versions, local generation marker and restored SQLite data; legacy callback outside Work and enrollment callback before any local write file:CreateTemp@2200,file:Open@3189 7054cf1c887f4b6b4e20b62fd94e17949770c675cbe7e8efe2accf7f20b1c7ba
internal/hostbox/restore.go ColdRestore.InstallEmpty authority C rev3=PROPOSED-seed; existing empty-bootstrap proof and local marker; legacy callback outside Work and enrollment callback before purge or marker creation f8c0ea4eaf3fa692abe52dd3b0799c9239bcb0d761c69644cc4be78d51d74c04
internal/hostbox/restore.go loadPinned authority C,J2,J4 rev3=PROPOSED-seed; exact selected LTX versions and original trailer/pin proof; validated bytes normalized for Litestream while retaining original database checksum for final comparison file:ReadAll@662 1e2b22c7c501d31cd1f2cb3be0fe419fa7e25afde87c6ecb43ca85edb7dbc3a4
-internal/hostbox/servinghandover.go ServingHandover.Recover authority C,J4 rev3=PROPOSED-seed; trusted serving transfer journals and pending root barriers; replay exact original transfer before prelaunch 7ec3bd1b929ca3b3fd7d20c53437740d54086fbb2ae1fc8c545dbca3e388bab1
+internal/hostbox/servinghandover.go ServingHandover.Recover authority C,J4 rev3=PROPOSED-seed; root lifecycle and retained recovery; independent crashed-controller recovery, per-journal errors retained, no agent evidence elevated 6b90bd31f43571b0203059a49909b80fd35d7fbd42d1d8fe63685c928c7de3e9
internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshot, exact stopped predecessor, canonical nested row, no-follow state identity, successor launch absence and private journal; fenced inventory/row transfer with retained old evidence registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5480,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5551,registry:ClaimRowID@5725,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5690,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5480,registry:Route@5551,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5633,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5482,whole:ColdRows@5553,whole:ColdRows@5635,whole:ColdRows@6216,whole:ColdRows@6226,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5488,whole:Route@5559,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5641 01b9e603a5f2a2476017777a7d02fafc76424d491201d6757170d66997fa481a
internal/hostbox/servinghandover.go ServingHandover.at authority C,J4 rev3=PROPOSED-seed; in-process interruption seam; nil in production 0178e5fcfb2cc194fb4f33f0f230b52849ddeb87a4b7c4e6cfb640e8b2f00b8a
internal/hostbox/servinghandover.go ServingHandover.validate authority C,J4 rev3=PROPOSED-seed; fresh serving claim, unchanged root-recorded inventory, exact recorded state-directory inode/type, complete route/timer set and production authority callback file:Exec@258,registry:ClaimIncarnation@2164,registry:ClaimOwner@1379,registry:ClaimOwner@2238,registry:ClaimOwnership@2132,registry:ClaimRowID@2273,registry:ClaimStamp@2207,registry:Name@2118,registry:Registration@2310,registry:Route@1033,registry:Route@1379,registry:Timers@2064,registry:Timers@2507,whole:ClaimOwnership@2138,whole:ColdRows@1035,whole:ColdRows@1381,whole:ColdRows@2066,whole:ColdRows@2509,whole:Route@1042,whole:Route@1388,whole:Timer@2073,whole:Timer@2516 2985e74f1c0aed6a6470e4ee4f03e794e32a97806542a51045a21e76ba7bce03
@@ -293,8 +324,8 @@ internal/hostguard/writerstop.go Guard.CompletedServingStops authority C rev3=PR
system.go runSystem 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 d1150dfb2baa129605aa219052e89ffc790069d56f4a4227c63ca9f40855a81b
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 232f9386027ac6cab0932da39ea44733ec20f1692010e26f5e4df6214d314440
-@coverage internal/host 9547dc56ff5626b8dc4d3d28cefb65e5b65af46ec0083c2e7a4800147fd5cbef
-@coverage internal/hostbox 3865bfeb63bcb671b919d1cfcf97b70405a0378ff6be4444648d12a678e67411
+@coverage internal/activator 41f0ad01ac54bae63f089ef8ec5bbfa9155bb76720d72b2d88563481de40fe5c
+@coverage internal/host 455146512e142affa2ba23b8cf3c49ad2028e78901c23e5ceaac96a1edddbad4
+@coverage internal/hostbox db890260533bdace6ad9a7c23dc1538cb919962894f005fc7c5a59a18e969429
@coverage internal/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
diff --git a/system_test.go b/system_test.go
index e9569f172..5ab7d71d4 100644
--- a/system_test.go
+++ b/system_test.go
@@ -2,6 +2,8 @@ package main
import (
"bytes"
+ "errors"
+ "os/exec"
"strings"
"testing"
"time"
@@ -274,3 +276,17 @@ func TestPrintSystemLayoutStatus(t *testing.T) {
t.Errorf("fleet: %q", b.String())
}
}
+
+func TestSystemFilesSeedCommandEntryRefusesBeforeEffects(t *testing.T) {
+ bin := buildCarlos(t)
+ for _, flags := range []string{"", "1"} {
+ cmd := exec.Command(bin, "system", "files", "seed", "--host", "seed.example")
+ cmd.Dir = t.TempDir()
+ cmd.Env = carlosTestEnv(t, "CARLOS_HOST_CLAIM="+flags, "CARLOS_HOST_CLAIM_ORIGINS="+flags, "CARLOS_MOVES_PHASE1=", "CARLOS_DEPLOYMENT_BUCKET=", "CARLOS_DEPLOYMENT_DIR="+t.TempDir(), "AWS_EC2_METADATA_DISABLED=true")
+ out, err := cmd.CombinedOutput()
+ var exit *exec.ExitError
+ if !errors.As(err, &exit) || exit.ExitCode() != 1 || !strings.Contains(string(out), "Files seed failed:") {
+ t.Fatalf("dispatch: %v %s", err, out)
+ }
+ }
+}