carlos / platform Public

WIP files seed checkpoint 24 (not for merge)

0R core collection, Task 2 enumeration, 0A shared exclusion and throttling, 1R settlement, Task 1 delegated one-shot foundation, real-origin seed plus two cold preparations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 991ae5a8f53b4dc01bea6cd5ff4d35afec558e0d parent 271ae93
36 files changed, +1992 −21
  • approved_moves_copy_test.go +1 −0
  • internal/activator/activator.go +9 −3
  • internal/activator/maintenance.go +6 −2
  • internal/activator/seed_diagnostic.go +51 −0
  • internal/activator/seed_diagnostic_test.go +48 −0
  • internal/activator/seed_exclusion.go +96 −0
  • internal/activator/seed_exclusion_test.go +93 −0
  • internal/activator/swaphold.go +3 −0
  • internal/host/claimassembly.go +6 −0
  • internal/host/claimfilesseed.go +266 −0
  • internal/host/claimfilesseed_command.go +167 −0
  • internal/host/claimfilesseed_delegation_linux.go +132 −0
  • internal/host/claimfilesseed_delegation_other.go +13 −0
  • internal/host/claimfilesseed_guard.go +5 −0
  • internal/host/claimfilesseed_integration_linux_test.go +171 −0
  • internal/host/claimfilesseed_restore_linux_test.go +18 −0
  • internal/host/claimfilesseed_retry.go +56 −0
  • internal/host/claimfilesseed_retry_linux_test.go +124 −0
  • internal/host/claimfilesseed_runtime.go +114 −0
  • internal/host/claimfilesseed_runtime_test.go +155 −0
  • internal/host/claimfilesseed_select.go +62 −0
  • internal/host/claimfilesseed_select_linux_test.go +25 −0
  • internal/host/claimlaunch_production.go +1 −1
  • internal/host/claimlaunch_root.go +10 −0
  • internal/host/claimservingcollection.go +58 −1
  • internal/host/claimservingcollection_linux_test.go +68 −0
  • internal/host/claimservingcollection_rootrunner_linux_test.go +17 −0
  • internal/host/claimservingmaintenance.go +3 −0
  • internal/host/claimservingpark_rootrunner_linux_test.go +35 −0
  • internal/host/claimservingpeer_rootrunner_linux_test.go +36 −1
  • internal/host/claimservingreboot_commit_linux_test.go +66 −0
  • internal/host/claimwakeassembly.go +13 −1
  • internal/provision/plan9_b8_census_test.go +1 −1
  • internal/provision/testdata/plan14-root-reads-seed.tsv +59 −10
  • system.go +3 −1
  • system_test.go +1 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	311.267s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.878s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.439s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.723s
ok  	github.com/carlosframework/platform/internal/activator	251.514s
ok  	github.com/carlosframework/platform/internal/agentsock	0.018s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.030s
ok  	github.com/carlosframework/platform/internal/appconfig	2.619s
ok  	github.com/carlosframework/platform/internal/apperr	0.076s
ok  	github.com/carlosframework/platform/internal/applog	0.130s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.552s
?   	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.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.711s
ok  	github.com/carlosframework/platform/internal/changelog	0.331s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	395.967s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.227s
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.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.419s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	27.448s
?   	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.131s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.013s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/host	167.331s
ok  	github.com/carlosframework/platform/internal/hostactors	0.425s
ok  	github.com/carlosframework/platform/internal/hostbox	2.438s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.544s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.017s
ok  	github.com/carlosframework/platform/internal/hostexec	5.188s
ok  	github.com/carlosframework/platform/internal/hostfence	0.030s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.832s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.017s
ok  	github.com/carlosframework/platform/internal/hostguard	1.031s
ok  	github.com/carlosframework/platform/internal/hostops	1.657s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.017s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.017s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.469s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.146s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.017s
ok  	github.com/carlosframework/platform/internal/infraview	0.016s
ok  	github.com/carlosframework/platform/internal/instancectl	4.176s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.752s
ok  	github.com/carlosframework/platform/internal/instancefs	0.024s
ok  	github.com/carlosframework/platform/internal/instancekey	0.018s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.024s
ok  	github.com/carlosframework/platform/internal/layout	1.586s
ok  	github.com/carlosframework/platform/internal/lease	9.867s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.024s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.182s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.808s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.083s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	21.267s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.806s
ok  	github.com/carlosframework/platform/internal/objstore	0.195s
ok  	github.com/carlosframework/platform/internal/ops	0.168s
ok  	github.com/carlosframework/platform/internal/park	0.196s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	18.183s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.099s
ok  	github.com/carlosframework/platform/internal/relay	7.292s
ok  	github.com/carlosframework/platform/internal/release	21.730s
ok  	github.com/carlosframework/platform/internal/rollout	3.716s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.009s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.565s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.599s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	12.901s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.613s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.238s
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.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
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.598s
=== 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 15:02:11 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEBC1C6376055B, 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/approved_moves_copy_test.go b/approved_moves_copy_test.go
index c4d349fce..5793d69b2 100644
--- a/approved_moves_copy_test.go
+++ b/approved_moves_copy_test.go
@@ -1162,6 +1162,7 @@ func TestMovesTextAppliedPlaceholders(t *testing.T) {
// copy" task appends the files it touched; TestMovesScanFilesCoverTheDiff
// fails when the list falls behind.
var movesScanFiles = []string{
+ "internal/host/relocationflush.go",
"internal/host/relocationinstall.go",
"internal/host/rootfloor_reconstruct.go",
// Claim-owned pages share the alert transport; keep their drafts in the copy census.
diff --git a/internal/activator/activator.go b/internal/activator/activator.go
index 9dfa8060a..524cbe537 100644
--- a/internal/activator/activator.go
+++ b/internal/activator/activator.go
@@ -1209,7 +1209,9 @@ type Service struct {
liveExecGate func(rt registry.Route, build string) error
liveGateSaid map[string]string
- fences map[string]uint64
+ fences map[string]uint64
+ seedExclusions map[string]*SeedExclusion
+ seedDiagnostics map[string]*seedDiagnostic
// fenceOwners names the owner of an owned fence (FenceAndStopAs), by
// host. A fence installed any other way has no entry. Under a.mu.
fenceOwners map[string]string
@@ -4924,7 +4926,9 @@ func (a *Service) sweepOnce() {
// otherwise every box sweeping a due entry would race a wake per tick
// (the lease would let exactly one win, but loudly, forever). Eleven:
// `if a.mesh != nil && !a.mesh.owns(rt.Host) { continue }`.
- log.Printf("activator: %s has a scheduled delivery due — waking it", rt.Host)
+ if a.allowFilesDiagnosticLocked(a.seedDiagnostics[rt.Host], "due") {
+ log.Printf("activator: %s has a scheduled delivery due — waking it", rt.Host)
+ }
// A sidecar row comes up as a WORKER (registry.DueInstances returns
// both kinds, because the due-time index is a sidecar's only timer
// wake source — nothing can dial one). Woken through the instance path
@@ -5408,7 +5412,9 @@ func (a *Service) serveWakeError(w http.ResponseWriter, r *http.Request, rt regi
// The wake failure is already the member's platform log error
// (this log line, shipped by logsink.go); marking it keeps the
// edge from filing the same failure a second time.
- log.Printf("activator: wake %s: %v", rt.Host, err)
+ if a.allowFilesDiagnostic(rt.Host, err, "http") {
+ log.Printf("activator: wake %s: %v", rt.Host, err)
+ }
edge.MarkAnswered(r)
http.Error(w, "instance failed to wake", http.StatusServiceUnavailable)
}
diff --git a/internal/activator/maintenance.go b/internal/activator/maintenance.go
index 14c710efd..ce8d24ee3 100644
--- a/internal/activator/maintenance.go
+++ b/internal/activator/maintenance.go
@@ -128,6 +128,10 @@ func (a *Service) fenceAndStop(ctx context.Context, host string, generation uint
}
return nil, ErrFenceNotInstalled
}
+ if a.seedExclusions[host] != nil {
+ a.mu.Unlock()
+ return nil, ErrFenceHeld
+ }
if preparation := a.tokenPreparations[host]; preparation != nil {
a.mu.Unlock()
select {
@@ -220,7 +224,7 @@ func (a *Service) LiftFence(host string, generation uint64) bool {
}
a.mu.Lock()
defer a.mu.Unlock()
- if a.fences[host] != generation {
+ if a.seedExclusions[host] != nil || a.fences[host] != generation {
return false
}
delete(a.fences, host)
@@ -238,7 +242,7 @@ func (a *Service) LiftFenceAs(host string, generation uint64, owner string) bool
}
a.mu.Lock()
defer a.mu.Unlock()
- if a.fences[host] != generation || a.fenceOwners[host] != owner {
+ if a.seedExclusions[host] != nil || a.fences[host] != generation || a.fenceOwners[host] != owner {
return false
}
delete(a.fences, host)
diff --git a/internal/activator/seed_diagnostic.go b/internal/activator/seed_diagnostic.go
new file mode 100644
index 000000000..ed232cbf8
--- /dev/null
+++ b/internal/activator/seed_diagnostic.go
@@ -0,0 +1,51 @@
+package activator
+
+import (
+ "errors"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "time"
+)
+
+// ServingFilesMissing preserves the ordinary no-evidence refusal while
+// giving diagnostics a host-incarnation key. It is not a retry policy.
+type ServingFilesMissing struct{ Incarnation string }
+
+func (e *ServingFilesMissing) Error() string { return hostfiles.ErrNoEvidence.Error() }
+func (e *ServingFilesMissing) Unwrap() error { return hostfiles.ErrNoEvidence }
+
+type seedDiagnostic struct {
+ incarnation string
+ at map[string]time.Time
+}
+
+func (a *Service) allowFilesDiagnostic(host string, err error, channel string) bool {
+ var missing *ServingFilesMissing
+ if !errors.As(err, &missing) {
+ return true
+ }
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.seedDiagnostics == nil {
+ a.seedDiagnostics = map[string]*seedDiagnostic{}
+ }
+ d := a.seedDiagnostics[host]
+ if d == nil || d.incarnation != missing.Incarnation {
+ d = &seedDiagnostic{incarnation: missing.Incarnation, at: map[string]time.Time{}}
+ a.seedDiagnostics[host] = d
+ }
+ return a.allowFilesDiagnosticLocked(d, channel)
+}
+func (a *Service) allowFilesDiagnosticLocked(d *seedDiagnostic, channel string) bool {
+ if d == nil {
+ return true
+ }
+ now := time.Now()
+ if a.now != nil {
+ now = a.now()
+ }
+ if at := d.at[channel]; !at.IsZero() && now.Before(at.Add(time.Minute)) {
+ return false
+ }
+ d.at[channel] = now
+ return true
+}
diff --git a/internal/activator/seed_diagnostic_test.go b/internal/activator/seed_diagnostic_test.go
new file mode 100644
index 000000000..7eeb78c5a
--- /dev/null
+++ b/internal/activator/seed_diagnostic_test.go
@@ -0,0 +1,48 @@
+package activator
+
+import (
+ "bytes"
+ "github.com/carlosframework/platform/internal/registry"
+ "log"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestUnseededDiagnosticsStayBoundedWhileEveryRequestRefuses(t *testing.T) {
+ var logs bytes.Buffer
+ old := log.Writer()
+ log.SetOutput(&logs)
+ defer log.SetOutput(old)
+ now := time.Unix(100, 0)
+ a := &Service{now: func() time.Time { return now }}
+ rt := registry.Route{Host: "b.example"}
+ missing := &ServingFilesMissing{Incarnation: "inc-1"}
+ events, due := 0, 0
+ for i := 0; i < 100; i++ {
+ w := httptest.NewRecorder()
+ a.serveWakeError(w, httptest.NewRequest("GET", "/", nil), rt, missing)
+ if w.Code != 503 {
+ t.Fatal(w.Code)
+ }
+ if _, ok := a.wakeFailureEvent(&actEntry{host: rt.Host}, missing); ok {
+ events++
+ }
+ a.mu.Lock()
+ if a.allowFilesDiagnosticLocked(a.seedDiagnostics[rt.Host], "due") {
+ due++
+ }
+ a.mu.Unlock()
+ }
+ if events != 1 || due != 1 || strings.Count(logs.String(), "activator: wake") != 1 {
+ t.Fatal(events, due, logs.String())
+ }
+ now = now.Add(time.Minute)
+ if !a.allowFilesDiagnostic(rt.Host, missing, "http") {
+ t.Fatal("suppressed next interval")
+ }
+ if !a.allowFilesDiagnostic(rt.Host, &ServingFilesMissing{Incarnation: "inc-2"}, "http") {
+ t.Fatal("suppressed next incarnation")
+ }
+}
diff --git a/internal/activator/seed_exclusion.go b/internal/activator/seed_exclusion.go
new file mode 100644
index 000000000..8aa9eeced
--- /dev/null
+++ b/internal/activator/seed_exclusion.go
@@ -0,0 +1,96 @@
+package activator
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/controlname"
+)
+
+// SeedExclusion is a process-local preparation fence. Only its original
+// executor may resume it after failed cleanup; it is never a wire identity.
+type SeedExclusion struct {
+ service *Service
+ host string
+}
+
+func (a *Service) AcquireSeedExclusion(ctx context.Context, host string, prior *SeedExclusion) (*SeedExclusion, error) {
+ if a == nil || ctx == nil || !controlname.ValidHostShape(host) {
+ return nil, ErrFenceNotInstalled
+ }
+ for {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ a.mu.Lock()
+ if a.shuttingDown {
+ a.mu.Unlock()
+ return nil, ErrFenceNotInstalled
+ }
+ if prior != nil {
+ ok := prior.service == a && prior.host == host && a.seedExclusions[host] == prior && a.fences[host] == ^uint64(0)
+ a.mu.Unlock()
+ if !ok {
+ return nil, ErrFenceHeld
+ }
+ return prior, nil
+ }
+ if a.fences[host] != 0 {
+ a.mu.Unlock()
+ return nil, ErrFenceHeld
+ }
+ wait := a.tokenPreparations[host]
+ if e := a.entries[host]; e != nil {
+ if e.state == actWaking {
+ wait = e.ready
+ }
+ if e.state == actStopping {
+ wait = e.stopped
+ }
+ }
+ if wait != nil {
+ a.mu.Unlock()
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-wait:
+ continue
+ }
+ }
+ token := &SeedExclusion{service: a, host: host}
+ if a.seedExclusions == nil {
+ a.seedExclusions = map[string]*SeedExclusion{}
+ }
+ if a.fences == nil {
+ a.fences = map[string]uint64{}
+ }
+ a.seedExclusions[host] = token
+ a.fences[host] = ^uint64(0)
+ a.mu.Unlock()
+ return token, nil
+ }
+}
+func (t *SeedExclusion) Check(host string) error {
+ if t == nil || t.service == nil || t.host != host {
+ return ErrFenceHeld
+ }
+ a := t.service
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.seedExclusions[host] != t || a.fences[host] != ^uint64(0) {
+ return ErrFenceHeld
+ }
+ return nil
+}
+func (t *SeedExclusion) Release() error {
+ if t == nil || t.service == nil {
+ return ErrFenceHeld
+ }
+ a := t.service
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ if a.seedExclusions[t.host] != t || a.fences[t.host] != ^uint64(0) {
+ return ErrFenceHeld
+ }
+ delete(a.seedExclusions, t.host)
+ delete(a.fences, t.host)
+ return nil
+}
diff --git a/internal/activator/seed_exclusion_test.go b/internal/activator/seed_exclusion_test.go
new file mode 100644
index 000000000..02e68c684
--- /dev/null
+++ b/internal/activator/seed_exclusion_test.go
@@ -0,0 +1,93 @@
+package activator
+
+import (
+ "context"
+ "errors"
+ "github.com/carlosframework/platform/internal/registry"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+func TestSeedExclusionWaitsForExistingPreparation(t *testing.T) {
+ for _, kind := range []string{"request", "timer", "stop"} {
+ t.Run(kind, func(t *testing.T) {
+ a := &Service{entries: map[string]*actEntry{}}
+ pending := make(chan struct{})
+ switch kind {
+ case "request":
+ a.entries["b.example"] = &actEntry{state: actWaking, ready: pending, wakeCancel: func() { t.Error("canceled existing wake") }}
+ case "timer":
+ a.tokenPreparations = map[string]chan struct{}{"b.example": pending}
+ case "stop":
+ a.entries["b.example"] = &actEntry{state: actStopping, stopped: pending}
+ }
+ ctx, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond)
+ defer cancel()
+ if _, err := a.AcquireSeedExclusion(ctx, "b.example", nil); !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("did not wait: %v", err)
+ }
+ if a.fences["b.example"] != 0 {
+ t.Fatal("fenced the existing preparation")
+ }
+ a.mu.Lock()
+ delete(a.entries, "b.example")
+ delete(a.tokenPreparations, "b.example")
+ close(pending)
+ a.mu.Unlock()
+ token, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := token.Check("b.example"); err != nil {
+ t.Fatal(err)
+ }
+ for _, serve := range []func(http.ResponseWriter, *http.Request, registry.Route, http.Handler) bool{a.ServeHTTP, a.ServeScheduled} {
+ response := httptest.NewRecorder()
+ called := false
+ handled := serve(response, httptest.NewRequest("GET", "http://b.example/", nil), registry.Route{Host: "b.example", Kind: "instance", Hibernate: true}, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
+ if !handled || response.Code != 503 || called {
+ t.Fatalf("request escaped exclusion: %v %d %v", handled, response.Code, called)
+ }
+ }
+ if _, err := a.PrepareScheduledToken("b.example", func() (string, error) { t.Fatal("timer entered seed"); return "", nil }); !errors.Is(err, ErrFenced) {
+ t.Fatal(err)
+ }
+ if _, err := a.PrepareScheduledToken("a.example", func() (string, error) { return "ok", nil }); err != nil {
+ t.Fatal("other host blocked", err)
+ }
+ if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil); !errors.Is(err, ErrFenceHeld) {
+ t.Fatal("duplicate accepted", err)
+ }
+ again, err := a.AcquireSeedExclusion(t.Context(), "b.example", token)
+ if err != nil || again != token {
+ t.Fatal("unsettled owner could not resume", err)
+ }
+ if err := token.Release(); err != nil {
+ t.Fatal(err)
+ }
+ if err := token.Check("b.example"); !errors.Is(err, ErrFenceHeld) {
+ t.Fatal("stale token accepted", err)
+ }
+ })
+ }
+}
+
+func TestSeedExclusionRefusesForeignFenceAndToken(t *testing.T) {
+ a := &Service{fences: map[string]uint64{"b.example": 7}}
+ if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", nil); !errors.Is(err, ErrFenceHeld) {
+ t.Fatal(err)
+ }
+ b := &Service{}
+ tok, err := b.AcquireSeedExclusion(t.Context(), "b.example", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := a.AcquireSeedExclusion(t.Context(), "b.example", tok); !errors.Is(err, ErrFenceHeld) {
+ t.Fatal(err)
+ }
+ if err := tok.Check("a.example"); !errors.Is(err, ErrFenceHeld) {
+ t.Fatal(err)
+ }
+}
diff --git a/internal/activator/swaphold.go b/internal/activator/swaphold.go
index 837efcb86..c55654d96 100644
--- a/internal/activator/swaphold.go
+++ b/internal/activator/swaphold.go
@@ -396,6 +396,9 @@ func (a *Service) pinExecReached(e *actEntry) bool {
// member's timeline. A pinned build that ran and failed gets the wording
// every failed wake gets.
func (a *Service) wakeFailureEvent(e *actEntry, err error) (string, bool) {
+ if !a.allowFilesDiagnostic(e.host, err, "event") {
+ return "", false
+ }
if e.pin == nil {
return memberSafeWakeReason(err), true
}
diff --git a/internal/host/claimassembly.go b/internal/host/claimassembly.go
index eb4f0169a..978c2ca00 100644
--- a/internal/host/claimassembly.go
+++ b/internal/host/claimassembly.go
@@ -10,6 +10,7 @@ import (
"strings"
"time"
+ "github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/controlsock"
"github.com/carlosframework/platform/internal/genselect"
"github.com/carlosframework/platform/internal/hostbox"
@@ -209,6 +210,11 @@ func (a ClaimAssembly) Build(ctx context.Context) (ClaimProviders, error) {
if wake == nil || wake.Read == nil || wake.Prerequisites == nil || wake.Admit == nil || wake.Prepare == nil || wake.StopForeign == nil || wake.FailedAdmission == nil {
return ClaimProviders{}, hostwrite.ErrUnavailable
}
+ if a.Mode == "agent" && a.wakeUnavailable == nil {
+ a.Wake.seedFactory = func(call context.Context, op *filesSeedOperation, exclusion *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) {
+ return newFilesSeedRuntime(call, a, source, writer, op, exclusion)
+ }
+ }
// Where claimed wakes lease (writer.Bucket, a.Wake.LeaseRegion above):
// the agent's build-state store reads and writes beside that lease.
wake.Bucket, wake.LeaseRegion = actor.Bucket, actor.Cloud.Region
diff --git a/internal/host/claimfilesseed.go b/internal/host/claimfilesseed.go
new file mode 100644
index 000000000..9847e3e41
--- /dev/null
+++ b/internal/host/claimfilesseed.go
@@ -0,0 +1,266 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+type filesSeedIncompleteError struct{ error }
+
+type filesSeedPending struct {
+ exclusion *activator.SeedExclusion
+ wake *ClaimWakeAssembly
+}
+type filesSeedExecutor struct {
+ assembly *ClaimAssembly
+ service *activator.Service
+ registry *registry.Registry
+ source *movesFilesSource
+ mu sync.Mutex
+ running bool
+ pending map[string]filesSeedPending
+}
+
+func (e *filesSeedExecutor) run(ctx context.Context, host string, out io.Writer) error {
+ if e == nil || e.assembly == nil || e.service == nil || e.registry == nil || e.source == nil || out == nil {
+ return hostbox.ErrUnsupported
+ }
+ e.mu.Lock()
+ if e.running {
+ e.mu.Unlock()
+ return activator.ErrFenceHeld
+ }
+ if e.pending == nil {
+ e.pending = map[string]filesSeedPending{}
+ }
+ e.running = true
+ e.mu.Unlock()
+ defer func() { e.mu.Lock(); e.running = false; e.mu.Unlock() }()
+ a := e.assembly
+ if a.Wake == nil || a.Wake.seedFactory == nil {
+ return hostbox.ErrUnsupported
+ }
+ actor, err := a.Identity.Verify(ctx)
+ if err != nil {
+ return err
+ }
+ candidates, err := filesSeedCandidates(ctx, a.Claims, actor.Box, host)
+ if err != nil {
+ return err
+ }
+ var failures []error
+ for _, o := range candidates {
+ if err := ctx.Err(); err != nil {
+ return filesSeedIncompleteError{errors.Join(append(failures, err)...)}
+ }
+ if err := e.host(ctx, o, out); err != nil {
+ failures = append(failures, err)
+ if _, writeErr := fmt.Fprintf(out, "Host %q: failed: %s.\n", o.Host, err); writeErr != nil {
+ return errors.Join(err, writeErr)
+ }
+ }
+ }
+ if err := errors.Join(failures...); err != nil {
+ return filesSeedIncompleteError{err}
+ }
+ return nil
+}
+
+func filesSeedRetryReport(out io.Writer, o hostclaim.Observed) error {
+ if o.Claim == nil || o.Claim.Holder == nil {
+ return nil
+ }
+ r := o.Claim.Holder.WakeRetry
+ if r == nil {
+ r = &hostclaim.WakeRetry{State: "none"}
+ }
+ _, err := fmt.Fprintf(out, "Host %q: wake retry episode %q, state %q, reserved %d of 5, remaining %d, retry after %q.\n", o.Host, r.EpisodeID, r.State, r.AttemptsReserved, 5-r.AttemptsReserved, r.NotBefore.Format(time.RFC3339))
+ return err
+}
+
+func (e *filesSeedExecutor) host(ctx context.Context, o hostclaim.Observed, out io.Writer) (resultErr error) {
+ host := o.Host
+ a := e.assembly
+ actor, err := a.Identity.Verify(ctx)
+ if err != nil {
+ return err
+ }
+ // Scope is decided from the claim before the registry is consulted.
+ if o.Absent || o.Claim == nil {
+ _, err = fmt.Fprintf(out, "Host %q: skipped; no claim.\n", o.Host)
+ return err
+ }
+ if o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindServing || o.Claim.Op.Exec != actor.Box || o.Claim.Op.ID != "" || o.Claim.Op.Phase != "" {
+ _, err = fmt.Fprintf(out, "Host %q: skipped; claim state %q, executor %q.\n", o.Host, o.Claim.Op.Kind, o.Claim.Op.Exec)
+ return err
+ }
+ row, err := e.registry.Get(o.Host)
+ if err != nil {
+ return err
+ }
+ if row == nil {
+ return hostbox.ErrChanged
+ }
+ if row.Replica {
+ _, err := fmt.Fprintf(out, "Host %q: skipped; replica files are not an authoritative source.\n", o.Host)
+ return err
+ }
+ if row.Seed != "" || row.Backing != "" || row.Unit != "" || row.IsSidecar() || row.Tunnel != "" {
+ return hostbox.ErrUnsupported
+ }
+ opened, err := e.source.Open(ctx, a.Claims, hostbox.RequestStore{Store: a.Data, Check: claimBoundIdentity(a.Identity.Verify, actor)}, actor)
+ if err != nil {
+ return err
+ }
+ files, ok := opened.(*movesFiles)
+ if !ok {
+ return hostbox.ErrUnsupported
+ }
+ if _, err = checkFilesSeedLayout(files, *row, claimEmptyBootstrap(o.Claim.Holder)); err != nil {
+ return err
+ }
+ pending := e.pending[o.Host]
+ retry := o.Claim.Holder.WakeRetry
+ needsSettlement := pending.exclusion != nil || retry != nil && retry.State == "attempting"
+ var token *activator.SeedExclusion
+ release := func() error {
+ if token == nil {
+ return nil
+ }
+ err := token.Release()
+ if err == nil {
+ delete(e.pending, o.Host)
+ token = nil
+ }
+ return err
+ }
+ defer func() {
+ if token != nil && e.pending[o.Host].exclusion == nil {
+ resultErr = errors.Join(resultErr, release())
+ }
+ }()
+ acquire := func() error {
+ var err error
+ token, err = e.service.AcquireSeedExclusion(ctx, o.Host, pending.exclusion)
+ if err != nil {
+ return err
+ }
+ fresh, readErr := hostclaim.Read(ctx, a.Claims, host)
+ if readErr == nil {
+ o = fresh
+ }
+ return readErr
+ }
+ if needsSettlement {
+ if err := acquire(); err != nil {
+ return err
+ }
+ if pending.wake != nil {
+ if err := pending.wake.Close(ctx); err != nil {
+ return err
+ }
+ }
+ settled, settleErr := settleFilesSeedRerun(ctx, *row, files, a.Wake.Retries, o, token)
+ err = settleErr
+ if err != nil {
+ e.pending[o.Host] = filesSeedPending{exclusion: token, wake: pending.wake}
+ return err
+ }
+ delete(e.pending, o.Host)
+ o = settled
+ }
+ selected, err := inspectFilesSeed(ctx, a.Claims, files, o, actor, row)
+ if err != nil {
+ return err
+ }
+ switch selected.State {
+ case "established":
+ if err := release(); err != nil {
+ return err
+ }
+ _, err = fmt.Fprintf(out, "Host %q: an established files manifest already exists; no seed was written.\n", o.Host)
+ return err
+ case "emptyBootstrap":
+ _, err = fmt.Fprintf(out, "Host %q: no files seed needed; the first wake starts empty.\n", o.Host)
+ return err
+ case "replica":
+ _, err = fmt.Fprintf(out, "Host %q: skipped; replica files are not an authoritative source.\n", o.Host)
+ return err
+ case "eligible":
+ default:
+ return hostbox.ErrUnsupported
+ }
+ if token == nil {
+ if err := acquire(); err != nil {
+ return err
+ }
+ selected, err = inspectFilesSeed(ctx, a.Claims, files, o, actor, row)
+ if err != nil {
+ return err
+ }
+ if selected.State != "eligible" {
+ return hostbox.ErrChanged
+ }
+ }
+ if err := filesSeedRetryReport(out, o); err != nil {
+ return err
+ }
+ defer func() {
+ reportCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), hostbox.RequestLimit)
+ defer cancel()
+ fresh, err := hostclaim.Read(reportCtx, a.Claims, o.Host)
+ if err == nil {
+ err = filesSeedRetryReport(out, fresh)
+ }
+ resultErr = errors.Join(resultErr, err)
+ }()
+ retry = o.Claim.Holder.WakeRetry
+ if retry != nil {
+ if retry.State != "open" || retry.AttemptsReserved >= 5 {
+ return hostclaim.ErrWrongState
+ }
+ delay := time.Until(retry.NotBefore)
+ if delay > 0 {
+ timer := time.NewTimer(delay)
+ defer timer.Stop()
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-timer.C:
+ }
+ }
+ }
+ _, op, err := newFilesSeedProvider(ctx, e.source, a.Claims, files.Data, actor, e.registry, selected)
+ if err != nil {
+ return err
+ }
+ runtime, wake, err := a.Wake.seedFactory(ctx, op, token)
+ if err != nil {
+ return err
+ }
+ if _, err = selectFilesSeedInputs(ctx, wake.Inputs, *row, selected); err != nil {
+ return err
+ }
+ admitted, settled, err := prepareFilesSeed(ctx, *row, runtime, o, token)
+ if !settled {
+ e.pending[o.Host] = filesSeedPending{exclusion: token, wake: wake}
+ }
+ if admitted.Claim != nil && admitted.Claim.Holder != nil {
+ _, writeErr := fmt.Fprintf(out, "Host %q: admitted files prefix %q; retain this line for interrupted-run cleanup.\n", o.Host, movesFilesPrefix(admitted.Claim.Holder.Location, admitted.Claim.Holder.Generation))
+ err = errors.Join(err, writeErr)
+ }
+ if err != nil {
+ return err
+ }
+ _, err = fmt.Fprintf(out, "Host %q: seeded.\n", o.Host)
+ return err
+}
diff --git a/internal/host/claimfilesseed_command.go b/internal/host/claimfilesseed_command.go
new file mode 100644
index 000000000..fa1b4112f
--- /dev/null
+++ b/internal/host/claimfilesseed_command.go
@@ -0,0 +1,167 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/signal"
+ "os/user"
+ "path/filepath"
+ "strconv"
+ "syscall"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/controlsock"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+var errFilesSeedDelegation = errors.New("run through carlos-edge.service with delegation enabled; a standalone shell cannot prepare this host")
+
+func parseFilesSeed(args []string) (string, error) {
+ if len(args) == 0 || args[0] != "seed" {
+ return "", hostbox.ErrUnsupported
+ }
+ fs := flag.NewFlagSet("system files seed", flag.ContinueOnError)
+ fs.SetOutput(io.Discard)
+ host := fs.String("host", "", "")
+ if err := fs.Parse(args[1:]); err != nil {
+ return "", err
+ }
+ if fs.NArg() != 0 {
+ return "", hostbox.ErrUnsupported
+ }
+ if *host != "" {
+ if _, err := hostclaim.Key(*host); err != nil {
+ return "", err
+ }
+ }
+ return *host, nil
+}
+
+func RunSystemFiles(args []string) {
+ ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
+ defer cancel()
+ if err := runSystemFiles(ctx, args, os.Stdout); err != nil {
+ log.Printf("Files seed failed: %s.", err)
+ os.Exit(filesSeedExitCode(err))
+ }
+}
+
+func filesSeedExitCode(err error) int {
+ if err == nil {
+ return 0
+ }
+ var incomplete filesSeedIncompleteError
+ if errors.As(err, &incomplete) {
+ return 2
+ }
+ return 1
+}
+
+func runSystemFiles(ctx context.Context, args []string, out io.Writer) error {
+ host, err := parseFilesSeed(args)
+ if err != nil {
+ return err
+ }
+ if !hostwrite.Enabled(os.Getenv) || os.Getenv("CARLOS_HOST_CLAIM_ORIGINS") != "1" {
+ return errors.New("CARLOS_HOST_CLAIM and CARLOS_HOST_CLAIM_ORIGINS must both be set; CARLOS_MOVES_PHASE1 is not required")
+ }
+ u, err := user.Lookup("carlos")
+ if err != nil {
+ return errFilesSeedDelegation
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil || uid == 0 || os.Geteuid() != uid {
+ return errFilesSeedDelegation
+ }
+ pid, err := filesSeedEdgePID(ctx)
+ if err != nil || pid != os.Getpid() {
+ return errFilesSeedDelegation
+ }
+ manager, err := filesSeedManager(ctx)
+ if err != nil {
+ return err
+ }
+ // This capability runs root's peer and delegated-cgroup validation before
+ // registry construction, classification, stop, lease or retry mutation.
+ root := NewClaimRoleRootClient(controlsock.Path)
+ if err := root.KindCapability(ctx, "files-seed"); err != nil {
+ return errors.Join(errFilesSeedDelegation, err)
+ }
+ return runFilesSeedOneShot(ctx, host, out, manager)
+}
+
+func runFilesSeedOneShot(ctx context.Context, host string, out io.Writer, manager *cgroup.Manager) (resultErr error) {
+ if manager == nil {
+ return errFilesSeedDelegation
+ }
+ ctx, err := claimEntryContext(ctx, os.Getenv, openDeployStore)
+ if err != nil {
+ return err
+ }
+ dd := dataDir()
+ reg, err := openServiceRegistry(filepath.Join(dd, "registry.db"))
+ if err != nil {
+ return err
+ }
+ safeToClose := true
+ defer func() {
+ if safeToClose {
+ resultErr = errors.Join(resultErr, reg.Close())
+ }
+ }()
+ if err := registerProductionClaimFiles(claimEntryEnv(ctx), dd, reg); err != nil {
+ return err
+ }
+ resources := ClaimRuntimeResources{Mode: "agent", Registry: reg, Manager: manager, DataDir: dd, OptDir: release.OptDir(), UnitsDir: "/etc/systemd/system"}
+ ctx = withClaimRuntimeResources(ctx, resources)
+ ctx, err = ensureProductionClaimAssembly(ctx)
+ if err != nil {
+ return err
+ }
+ entry, _ := ctx.Value(claimEntryKey{}).(claimEntry)
+ a := entry.Assembly
+ if a == nil {
+ return hostbox.ErrUnsupported
+ }
+ var executor *filesSeedExecutor
+ defer func() {
+ cleanup, cancel := context.WithTimeout(context.WithoutCancel(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 safeToClose {
+ closeErr := a.Close(cleanup)
+ resultErr = errors.Join(resultErr, closeErr)
+ if closeErr != nil {
+ safeToClose = false
+ }
+ }
+ }()
+ if _, err := a.Build(ctx); err != nil {
+ return err
+ }
+ source, ok := productionClaimFilesSource().(*movesFilesSource)
+ if !ok {
+ return hostbox.ErrUnsupported
+ }
+ executor = &filesSeedExecutor{assembly: a, service: &activator.Service{}, registry: reg, source: source}
+ return executor.run(ctx, host, out)
+}
diff --git a/internal/host/claimfilesseed_delegation_linux.go b/internal/host/claimfilesseed_delegation_linux.go
new file mode 100644
index 000000000..a347220c0
--- /dev/null
+++ b/internal/host/claimfilesseed_delegation_linux.go
@@ -0,0 +1,132 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "fmt"
+ "github.com/carlosframework/platform/internal/cgroup"
+ "golang.org/x/sys/unix"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func filesSeedEdgePID(ctx context.Context) (int, error) {
+ ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
+ defer cancel()
+ b, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "MainPID", "--value", "carlos-edge.service").Output()
+ if err != nil {
+ return 0, err
+ }
+ pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
+ if err != nil || pid <= 0 {
+ return 0, ErrClaimStopIdentity
+ }
+ return pid, nil
+}
+
+// Root repeats both the service identity and the kernel process identity.
+// Capability supplies no path or PID from the caller and authorizes no write.
+func verifyFilesSeedDelegation(ctx context.Context, peer StopPeer) error {
+ if os.Geteuid() != 0 || peer.UID == 0 || peer.PID <= 0 {
+ return ErrClaimStopIdentity
+ }
+ check := func() error {
+ pid, err := filesSeedEdgePID(ctx)
+ if err != nil {
+ return err
+ }
+ if pid != peer.PID {
+ return ErrClaimStopIdentity
+ }
+ id, err := (productionStopControllers{}).VerifiedController(ctx, pid)
+ if err != nil {
+ return err
+ }
+ if id != peer.ControllerID {
+ return ErrClaimStopIdentity
+ }
+ return nil
+ }
+ if err := check(); 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
+ }
+ first, err := inspectClaimContainment(rel)
+ if err != nil {
+ return err
+ }
+ fd, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
+ if err != nil {
+ return err
+ }
+ defer unix.Close(fd)
+ var st unix.Stat_t
+ if err := unix.Fstat(fd, &st); err != nil {
+ return err
+ }
+ if st.Uid != peer.UID || uint64(st.Dev) != first.Device || st.Ino != first.Inode {
+ return ErrClaimStopIdentity
+ }
+ controls, err := os.ReadFile(filepath.Join(root, "cgroup.subtree_control"))
+ if err != nil {
+ return err
+ }
+ enabled := map[string]bool{}
+ for _, c := range strings.Fields(string(controls)) {
+ enabled[c] = true
+ }
+ for _, c := range []string{"cpu", "memory", "io", "pids"} {
+ if !enabled[c] {
+ return ErrClaimStopIdentity
+ }
+ }
+ groups, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", peer.PID))
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(string(groups)) != "0::/"+filepath.ToSlash(filepath.Join(rel, "supervise")) {
+ return ErrClaimStopIdentity
+ }
+ second, err := inspectClaimContainment(rel)
+ if err != nil {
+ return err
+ }
+ if first != second {
+ return ErrClaimStopIdentity
+ }
+ if err := check(); err != nil {
+ return err
+ }
+ return ctx.Err()
+}
+
+func filesSeedManager(ctx context.Context) (*cgroup.Manager, error) {
+ root, err := agentUnitCgroup(ctx)
+ if err != nil {
+ return nil, errFilesSeedDelegation
+ }
+ body, err := os.ReadFile("/proc/self/cgroup")
+ if err != nil {
+ return nil, err
+ }
+ rel, err := filepath.Rel("/sys/fs/cgroup", root)
+ if err != nil || !filepath.IsLocal(rel) || rel == "." {
+ return nil, errFilesSeedDelegation
+ }
+ if strings.TrimSpace(string(body)) != "0::/"+filepath.ToSlash(rel) {
+ return nil, errFilesSeedDelegation
+ }
+ return cgroup.SetupAtRetaining(root, func(string, ...any) {})
+}
diff --git a/internal/host/claimfilesseed_delegation_other.go b/internal/host/claimfilesseed_delegation_other.go
new file mode 100644
index 000000000..c4ca784d7
--- /dev/null
+++ b/internal/host/claimfilesseed_delegation_other.go
@@ -0,0 +1,13 @@
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/cgroup"
+)
+
+func filesSeedEdgePID(context.Context) (int, error) { return 0, ErrClaimStopUnavailable }
+func verifyFilesSeedDelegation(context.Context, StopPeer) error { return ErrClaimStopUnavailable }
+
+func filesSeedManager(context.Context) (*cgroup.Manager, error) { return nil, errFilesSeedDelegation }
diff --git a/internal/host/claimfilesseed_guard.go b/internal/host/claimfilesseed_guard.go
index aede400c6..bf9d76dc8 100644
--- a/internal/host/claimfilesseed_guard.go
+++ b/internal/host/claimfilesseed_guard.go
@@ -2,6 +2,8 @@ package host
import (
"context"
+ "errors"
+ "github.com/carlosframework/platform/internal/activator"
"reflect"
"github.com/carlosframework/platform/internal/hostbox"
@@ -63,6 +65,9 @@ func servingFilesPrerequisites(claims release.Store, files ClaimFilesManifest) f
return hostbox.ErrUnsupported
}
if err := f.checkServingFilesEvidence(ctx, o, rt); err != nil {
+ if errors.Is(err, hostfiles.ErrNoEvidence) && o.Claim.Holder != nil {
+ return &activator.ServingFilesMissing{Incarnation: o.Claim.Holder.Incarnation}
+ }
return err
}
fresh, err := hostclaim.Read(ctx, claims, o.Host)
diff --git a/internal/host/claimfilesseed_integration_linux_test.go b/internal/host/claimfilesseed_integration_linux_test.go
new file mode 100644
index 000000000..7669e6b12
--- /dev/null
+++ b/internal/host/claimfilesseed_integration_linux_test.go
@@ -0,0 +1,171 @@
+//go:build linux
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func runFilesSeedRealPreparation(t *testing.T, p *servingPreparation) {
+ t.Helper()
+ f := p.f
+ f.actor.Bucket = f.f.bucket
+ f.actor.Cloud.Region = "eu-west-1"
+ f.actor.HomeRegion = "ie"
+ f.actor.FleetHomeRegion = "ie"
+ p.local.Actor = f.actor
+ rt := registry.Route{Host: recoveryHost, Kind: "instance", Hibernate: true, Account: "bab", App: "jam", DBPath: f.db, Socket: provision.NestedSocket(recoveryHost)}
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "seed.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.SetNextDue(rt.Host, 777); err != nil {
+ t.Fatal(err)
+ }
+ filesDir := filepath.Join(filepath.Dir(f.db), rt.Host)
+ if err := os.MkdirAll(filesDir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(filesDir, "payload"), []byte("real seeded bytes"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(filepath.Dir(f.db), rt.Host+".env"), []byte("KEY=value\n"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ source := &movesFilesSource{DataDir: f.f.guard.DataDir, Routes: reg, newFiles: func(context.Context, string, string) (release.Store, error) {
+ return casBucketStore{b: f.f.data.(*leasetest.VersionedBucket), bucket: f.f.bucket}, nil
+ }}
+ a := p.assembly
+ a.Data = f.f.data.(*leasetest.VersionedBucket)
+ a.Identity = handoverRecoveryIdentity(t, f.actor)
+ a.Wake.seedFactory = func(ctx context.Context, op *filesSeedOperation, token *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) {
+ runtime, wake, err := newFilesSeedRuntime(ctx, a, p.source, p.writer, op, token)
+ if err != nil {
+ return runtime, wake, err
+ }
+ if f.f.guard.DataDir != "/data" {
+ // Local fixture omits only installed /data launch-shape preflight.
+ runtime.Prerequisites = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) error {
+ if err := wake.ServingPrerequisites(ctx, rt, o); err != nil {
+ return err
+ }
+ in, err := wake.Inputs.Select(ctx, rt, o, policy)
+ if err != nil {
+ return err
+ }
+ if err := wake.stopPredecessor(ctx, o); err != nil {
+ return err
+ }
+ held, err := lease.Acquire(ctx, f.f.bucket, o.Claim.Holder.Location, wake.LeaseRegion)
+ if err != nil {
+ return err
+ }
+ live, cancel := context.WithCancel(ctx)
+ done := make(chan struct{})
+ go func() { <-live.Done(); close(done) }()
+ wake.pending[rt.Host] = &claimWakeAttempt{owner: wake, observed: o, inputs: in, held: held, ctx: live, cancel: cancel, done: done}
+ return nil
+ }
+ }
+ return runtime, wake, nil
+ }
+ openedSelection, err := source.Open(t.Context(), f.f.claims, f.f.data.(*leasetest.VersionedBucket), f.actor)
+ if err != nil {
+ t.Fatal(err)
+ }
+ classification, err := inspectFilesSeed(t.Context(), f.f.claims, openedSelection.(*movesFiles), f.f.read(), f.actor, &rt)
+ if err != nil {
+ t.Fatal("classification", err)
+ }
+ if _, err := selectFilesSeedInputs(t.Context(), p.owners, rt, classification); err != nil {
+ t.Fatal("input selection", err)
+ }
+ executor := &filesSeedExecutor{assembly: &a, service: &activator.Service{}, registry: reg, source: source}
+ var output bytes.Buffer
+ if err := executor.run(t.Context(), rt.Host, &output); err != nil {
+ t.Fatalf("seed: %v\n%s", err, output.String())
+ }
+ seeded := f.f.read()
+ if seeded.Claim.Origins == nil || seeded.Claim.Holder.WakeRetry.AttemptsReserved != 1 || seeded.Claim.Holder.WakeRetry.State != "open" {
+ t.Fatal("seed did not settle one real admission", seeded)
+ }
+ if len(executor.pending) != 0 {
+ t.Fatal("seed retained its exclusion")
+ }
+ before := seeded.ETag
+ if err := executor.run(t.Context(), rt.Host, &output); err != nil {
+ t.Fatal("established rerun", err)
+ }
+ if f.f.read().ETag != before {
+ t.Fatal("established rerun mutated claim")
+ }
+ opened, err := source.Open(t.Context(), f.f.claims, f.f.data.(*leasetest.VersionedBucket), f.actor)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ordinary := opened.(*movesFiles)
+ for wake := 0; wake < 2; wake++ {
+ f.f.now = f.f.now.Add(time.Minute)
+ normal := newServingPreparation(t, f, "boot", "controller", func(w *ClaimWakeAssembly) {
+ w.Inputs.(*claimServingOwners).Sources.Files = ordinary
+ w.ServingPrerequisites = servingFilesPrerequisites(f.f.claims, ordinary)
+ })
+ admitted, start := normal.prepare(t)
+ if err := start.Restore(t.Context()); err != nil {
+ t.Fatal("cold restore", wake, err)
+ }
+ if err := start.Reserve(t.Context(), 0); err != nil {
+ t.Fatal(err)
+ }
+ if err := start.Materialize(t.Context()); err != nil {
+ t.Fatal("ordinary C8", wake, err)
+ }
+ prefix := movesFilesPrefix(admitted.Claim.Holder.Location, admitted.Claim.Holder.Generation)
+ manifest, found, err := hostfiles.Load(t.Context(), ordinary.Files, prefix)
+ if err != nil || !found {
+ t.Fatal(err)
+ }
+ if _, ok := manifest.Entries["payload"]; !ok {
+ t.Fatal("files lost at cold wake")
+ }
+ b, err := os.ReadFile(filepath.Join(filesDir, "payload"))
+ if err != nil || string(b) != "real seeded bytes" {
+ t.Fatal("restored files", string(b), err)
+ }
+ if err := start.Failed(t.Context(), 0); err != nil {
+ t.Fatal(err)
+ }
+ if err := normal.runtime.FailedAdmission(t.Context(), admitted, hostclaim.ErrWrongState); err != nil {
+ t.Fatal(err)
+ }
+ if ordinary.Seed != nil {
+ t.Fatal("ordinary provider armed")
+ }
+ }
+ if f.f.read().Claim.Holder.WakeRetry.AttemptsReserved != 3 {
+ t.Fatal("attempt budget changed")
+ }
+ t.Log(output.String())
+}
+
+func TestFilesSeedRealOriginPreparation(t *testing.T) {
+ runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller")
+}
diff --git a/internal/host/claimfilesseed_restore_linux_test.go b/internal/host/claimfilesseed_restore_linux_test.go
index 40e49260e..080fb483a 100644
--- a/internal/host/claimfilesseed_restore_linux_test.go
+++ b/internal/host/claimfilesseed_restore_linux_test.go
@@ -36,6 +36,9 @@ import (
// transport. Initial installation, kernel checks and launch preflight are
// explicit seams; it does not claim an installed-unit or privileged witness.
type servingPreparation struct {
+ assembly ClaimAssembly
+ source hostbox.WorkSource
+ writer hostbox.Writer
f *servingRootFixture
owners *claimServingOwners
wake *ClaimWakeAssembly
@@ -130,6 +133,7 @@ func newServingPreparation(t *testing.T, f *servingRootFixture, boot, controller
for _, fn := range configure {
fn(w)
}
+ p.assembly, p.source, p.writer = a, source, writer
p.runtime, err = w.Build(t.Context(), source, writer)
if err != nil {
t.Fatal(err)
@@ -212,6 +216,10 @@ func TestFilesSeedOrdinaryPreparationRestore(t *testing.T) {
}
func runFilesSeedOrdinaryPreparation(t *testing.T, failure, restart string, mounted ...bool) {
+ if failure == "real-seed" {
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ }
t.Setenv("CARLOS_HOST_CLAIM", "1")
t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
t.Setenv("CARLOS_MOVES_PHASE1", "")
@@ -393,7 +401,17 @@ func runFilesSeedOrdinaryPreparation(t *testing.T, failure, restart string, moun
t.Fatal(err)
}
}
+ if failure == "real-seed" {
+ f.actor.Bucket = f.f.bucket
+ f.actor.Cloud.Region = "eu-west-1"
+ f.actor.HomeRegion = "ie"
+ f.actor.FleetHomeRegion = "ie"
+ }
p := newServingPreparation(t, f, "boot", "controller")
+ if failure == "real-seed" {
+ runFilesSeedRealPreparation(t, p)
+ return
+ }
var legacy servingParkRecord
var baseline *genselect.Plan
for i := 0; i < 2; i++ {
diff --git a/internal/host/claimfilesseed_retry.go b/internal/host/claimfilesseed_retry.go
new file mode 100644
index 000000000..ba02d5d56
--- /dev/null
+++ b/internal/host/claimfilesseed_retry.go
@@ -0,0 +1,56 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func settleFilesSeedRerun(ctx context.Context, rt registry.Route, files *movesFiles, retries activator.ServingRetries, expected hostclaim.Observed, exclusion *activator.SeedExclusion) (hostclaim.Observed, error) {
+ if err := exclusion.Check(rt.Host); err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if expected.Host != rt.Host || expected.Claim == nil || expected.Claim.Holder == nil {
+ return hostclaim.Observed{}, hostbox.ErrChanged
+ }
+ if _, err := checkFilesSeedLayout(files, rt, claimEmptyBootstrap(expected.Claim.Holder)); err != nil {
+ return hostclaim.Observed{}, err
+ }
+ fresh, err := retries.SettleAbandoned(ctx, expected)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if err := exclusion.Check(rt.Host); err != nil {
+ return hostclaim.Observed{}, err
+ }
+ return fresh, nil
+}
+
+func (s *filesSeedOperation) prerequisites(exclusion *activator.SeedExclusion) func(context.Context, registry.Route, hostclaim.Observed) error {
+ return func(ctx context.Context, rt registry.Route, o hostclaim.Observed) error {
+ if err := exclusion.Check(rt.Host); err != nil {
+ return err
+ }
+ if s == nil || s.ctx == nil || s.ctx.Err() != nil || s.selected.State != "eligible" || o.Host != s.selected.Observation.Host || o.ETag != s.selected.Observation.ETag || s.bound != (servingArtifactIdentity{}) {
+ return hostbox.ErrChanged
+ }
+ if err := s.files.checkServingFilesEvidence(ctx, o, rt); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ if err == nil {
+ return hostfiles.ErrManifestExists
+ }
+ return err
+ }
+ selected, err := inspectFilesSeed(ctx, s.claims, s.files, o, s.actor, &rt)
+ if err != nil {
+ return err
+ }
+ if selected.State != "eligible" {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }
+}
diff --git a/internal/host/claimfilesseed_retry_linux_test.go b/internal/host/claimfilesseed_retry_linux_test.go
new file mode 100644
index 000000000..ab2618745
--- /dev/null
+++ b/internal/host/claimfilesseed_retry_linux_test.go
@@ -0,0 +1,124 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func TestFilesSeedPrivateBypassRequiresExactExclusionAndAbsence(t *testing.T) {
+ f := newFilesSeedFixture(t)
+ selected, err := f.inspect()
+ if err != nil {
+ t.Fatal(err)
+ }
+ op := &filesSeedOperation{ctx: t.Context(), claims: f.claims, actor: f.actor, files: f.files, selected: selected}
+ svc := &activator.Service{}
+ token, err := svc.AcquireSeedExclusion(t.Context(), filesHost, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ guard := op.prerequisites(token)
+ if err := guard(t.Context(), f.route, f.observed); err != nil {
+ t.Fatal(err)
+ }
+ if err := servingFilesPrerequisites(f.claims, f.files)(t.Context(), f.route, f.observed); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatal("ordinary guard bypassed", err)
+ }
+ if err := op.prerequisites(nil)(t.Context(), f.route, f.observed); err == nil {
+ t.Fatal("no exclusion")
+ }
+ bad := f.route
+ bad.DBPath = "/wrong"
+ if err := guard(t.Context(), bad, f.observed); err == nil {
+ t.Fatal("layout bypassed")
+ }
+ f.data.Put("data", hostfiles.ManifestKey(movesFilesPrefix(f.observed.Claim.Holder.Location, "")), []byte("{}"))
+ if err := guard(t.Context(), f.route, f.observed); err == nil {
+ t.Fatal("presence bypassed")
+ }
+ if err := token.Release(); err != nil {
+ t.Fatal(err)
+ }
+ if err := guard(t.Context(), f.route, f.observed); err == nil {
+ t.Fatal("released exclusion")
+ }
+}
+
+func TestFilesSeedRerunSettlementRequiresExclusion(t *testing.T) {
+ for _, fault := range []string{"none", "live-controller", "missing-record", "no-exclusion", "canceled"} {
+ t.Run(fault, func(t *testing.T) {
+ f := newRecoveryFixture(t)
+ if fault == "missing-record" {
+ f.recordPrelaunch = false
+ }
+ old := f.controller("boot-1", "controller-1", 4101, "start-1")
+ o := old.admit(t, f)
+ if fault != "missing-record" {
+ if _, err := old.runtime.Prepare(t.Context(), registry.Route{Host: recoveryHost}, o, recoveryPolicy); err != nil {
+ t.Fatal(err)
+ }
+ }
+ current := f.controller("boot-1", "controller-2", 4202, "start-2")
+ dir := t.TempDir()
+ rt := registry.Route{Host: recoveryHost, DBPath: provision.NestedDBPath(dir, recoveryHost)}
+ if err := os.MkdirAll(filepath.Dir(rt.DBPath), 0700); err != nil {
+ t.Fatal(err)
+ }
+ files := &movesFiles{DataDir: dir, EnvFile: func(host, db string) (claimEnvFile, error) {
+ return claimEnvFile{Nested: true, Dir: filepath.Dir(db), Name: host + ".env"}, nil
+ }}
+ svc := &activator.Service{}
+ token, err := svc.AcquireSeedExclusion(t.Context(), rt.Host, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fault == "no-exclusion" {
+ token = nil
+ }
+ f.kernel.alive = fault == "live-controller"
+ ctx := t.Context()
+ if fault == "canceled" {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithCancel(ctx)
+ cancel()
+ }
+ got, err := settleFilesSeedRerun(ctx, rt, files, current.wake.Retries, f.read(), token)
+ if fault != "none" {
+ if err == nil {
+ t.Fatal("unproved settlement accepted")
+ }
+ if f.retry().State != "attempting" {
+ t.Fatal("failed settlement changed debt")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ retry := got.Claim.Holder.WakeRetry
+ if retry.State != "open" || retry.AttemptsReserved != 1 || retry.Attempt != nil {
+ t.Fatal("not settled without refund", retry)
+ }
+ if len(f.receipts()) != 1 || f.receipts()[0].ControllerID != "controller-1" {
+ t.Fatal("wrong stop tuple", f.receipts())
+ }
+ if _, err = settleFilesSeedRerun(ctx, rt, files, current.wake.Retries, got, token); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = settleFilesSeedRerun(ctx, rt, files, current.wake.Retries, o, token); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatal("stale rerun", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/claimfilesseed_runtime.go b/internal/host/claimfilesseed_runtime.go
new file mode 100644
index 000000000..e242f2755
--- /dev/null
+++ b/internal/host/claimfilesseed_runtime.go
@@ -0,0 +1,114 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/registry"
+ "time"
+)
+
+type filesSeedRuntimeFactory func(context.Context, *filesSeedOperation, *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error)
+
+func newFilesSeedRuntime(ctx context.Context, a ClaimAssembly, source hostbox.WorkSource, writer hostbox.Writer, op *filesSeedOperation, exclusion *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) {
+ if a.Wake == nil || op == nil || a.wakeUnavailable != nil {
+ return nil, nil, hostbox.ErrUnsupported
+ }
+ base := a.Wake
+ var serving *claimServingOwners
+ switch in := base.Inputs.(type) {
+ case ClaimOwnerDispatch:
+ serving, _ = in.Serving.Inputs.(*claimServingOwners)
+ case *claimServingOwners:
+ serving = in
+ }
+ if serving == nil {
+ return nil, nil, hostbox.ErrUnsupported
+ }
+ sources := serving.Sources
+ sources.Files = op.files
+ private := &claimServingOwners{HomeRegion: serving.HomeRegion, Data: serving.Data, Bucket: serving.Bucket, Region: serving.Region, Box: serving.Box, NewID: serving.NewID, Now: serving.Now, Sources: sources}
+ w := &ClaimWakeAssembly{Roles: base.Roles, LeaseRegion: base.LeaseRegion, Producers: base.Producers, Inputs: private, Manager: base.Manager, Retries: base.Retries, BeforeAdmission: base.BeforeAdmission, RetireArtifacts: base.RetireArtifacts, RecoverArtifacts: base.RecoverArtifacts, RequireServingEnrollment: base.RequireServingEnrollment, BeforeLegacyInstall: base.BeforeLegacyInstall, BeforeInstallLocked: base.BeforeInstallLocked, PrepareState: base.PrepareState, Prelaunch: base.Prelaunch, Launch: base.Launch, Ready: base.Ready, ObserveLoss: base.ObserveLoss}
+ w.Retain = w.retainCaptured
+ w.ServingPrerequisites = op.prerequisites(exclusion)
+ w.AfterAdmission = func(ctx context.Context, prior, current hostclaim.Observed) error {
+ if err := base.AfterAdmission(ctx, prior, current); err != nil {
+ return err
+ }
+ return op.admitted(ctx, current)
+ }
+ w.Retries.Inputs = private.launchDigest
+ w.Retries.ExactHealth = w.exactHealth
+ w.Retries.BeforeReserve = func(ctx context.Context, rt registry.Route, o hostclaim.Observed) error {
+ if err := w.ServingPrerequisites(ctx, rt, o); err != nil {
+ return err
+ }
+ return claimServingRoom(op.files.DataDir)
+ }
+ a.Wake = w
+ a.bindClaimWakeStop()
+ writer.AdmissionEvidence = private.admissionEvidence
+ runtime, err := w.Build(ctx, source, writer)
+ return runtime, w, err
+}
+
+// The seed follows ordinary cold preparation and spends its one attempt. It
+// never calls any launch, publication, replication or health closure.
+func prepareFilesSeed(ctx context.Context, rt registry.Route, runtime *activator.ClaimRuntime, o hostclaim.Observed, exclusion *activator.SeedExclusion) (admitted hostclaim.Observed, settled bool, resultErr error) {
+ if err := exclusion.Check(rt.Host); err != nil {
+ return admitted, false, err
+ }
+ if runtime == nil || runtime.Read == nil || runtime.Prerequisites == nil || runtime.Admit == nil || runtime.Prepare == nil || runtime.FailedAdmission == nil {
+ return admitted, false, hostbox.ErrUnsupported
+ }
+ policy := activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second}
+ cleanupObservation := o
+ var start activator.ClaimStart
+ defer func() {
+ cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
+ defer cancel()
+ var stopErr error
+ if start.Failed != nil {
+ stopErr = start.Failed(cleanup, 0)
+ }
+ closeErr := runtime.FailedAdmission(cleanup, cleanupObservation, hostbox.ErrChanged)
+ fresh, readErr := runtime.Read(cleanup, o.Host)
+ if readErr == nil {
+ if fresh.Claim == nil || fresh.Claim.Holder == nil || o.Claim == nil || o.Claim.Holder == nil || fresh.Claim.Holder.Incarnation != o.Claim.Holder.Incarnation || fresh.Claim.Holder.WakeRetry != nil && fresh.Claim.Holder.WakeRetry.State == "attempting" {
+ readErr = hostbox.ErrChanged
+ }
+ }
+ closeErr = errors.Join(closeErr, readErr)
+ settled = stopErr == nil && closeErr == nil
+ resultErr = errors.Join(resultErr, stopErr, closeErr)
+ }()
+ if err := runtime.Prerequisites(ctx, rt, o, policy); err != nil {
+ return admitted, false, err
+ }
+ var err error
+ admitted, err = runtime.Admit(ctx, rt, o, policy)
+ if err != nil {
+ return admitted, false, err
+ }
+ cleanupObservation = admitted
+ start, err = runtime.Prepare(ctx, rt, admitted, policy)
+ if err != nil {
+ return admitted, false, err
+ }
+ if start.Restore == nil || start.Reserve == nil || start.Materialize == nil || start.Failed == nil {
+ return admitted, false, hostbox.ErrUnsupported
+ }
+ if err = start.Restore(ctx); err != nil {
+ return admitted, false, err
+ }
+ if err = start.Reserve(ctx, 0); err != nil {
+ return admitted, false, err
+ }
+ if err = exclusion.Check(rt.Host); err != nil {
+ return admitted, false, err
+ }
+ resultErr = start.Materialize(ctx)
+ return admitted, false, resultErr
+}
diff --git a/internal/host/claimfilesseed_runtime_test.go b/internal/host/claimfilesseed_runtime_test.go
new file mode 100644
index 000000000..eaef16a78
--- /dev/null
+++ b/internal/host/claimfilesseed_runtime_test.go
@@ -0,0 +1,155 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "os/exec"
+ "reflect"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func TestFilesSeedPreparationOrdersAndSettles(t *testing.T) {
+ for _, failure := range []string{"none", "prerequisites", "admit", "prepare", "restore", "reserve", "materialize", "settle"} {
+ t.Run(failure, func(t *testing.T) {
+ var events []string
+ mark := func(s string) error {
+ events = append(events, s)
+ if failure == s {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }
+ obs := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}}
+ svc := &activator.Service{}
+ token, err := svc.AcquireSeedExclusion(t.Context(), obs.Host, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r := &activator.ClaimRuntime{
+ Read: func(context.Context, string) (hostclaim.Observed, error) { return obs, nil },
+ Prerequisites: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error {
+ return mark("prerequisites")
+ },
+ Admit: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) {
+ return obs, mark("admit")
+ },
+ FailedAdmission: func(ctx context.Context, _ hostclaim.Observed, _ error) error {
+ if ctx.Err() != nil {
+ t.Fatal("canceled cleanup")
+ }
+ return mark("cleanup")
+ },
+ Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) {
+ if err := mark("prepare"); err != nil {
+ return activator.ClaimStart{}, err
+ }
+ return activator.ClaimStart{Restore: func(context.Context) error { return mark("restore") }, Reserve: func(_ context.Context, n int) error {
+ if n != 0 {
+ t.Fatal("additional attempt")
+ }
+ return mark("reserve")
+ }, Materialize: func(context.Context) error { return mark("materialize") }, Failed: func(context.Context, int) error { return mark("settle") }, Build: func() *exec.Cmd { t.Fatal("seed launched tenant"); return nil }, Healthy: func(context.Context, int, *exec.Cmd) error { t.Fatal("seed became healthy"); return nil }}, nil
+ },
+ }
+ _, settled, err := prepareFilesSeed(t.Context(), registry.Route{Host: obs.Host}, r, obs, token)
+ if failure == "none" {
+ if err != nil || !settled {
+ t.Fatal(err, settled)
+ }
+ want := []string{"prerequisites", "admit", "prepare", "restore", "reserve", "materialize", "settle", "cleanup"}
+ if !reflect.DeepEqual(events, want) {
+ t.Fatal(events)
+ }
+ } else if err == nil || settled == (failure == "settle") {
+ t.Fatal("wrong cleanup outcome", events, err, settled)
+ }
+ if err := token.Check(obs.Host); err != nil {
+ t.Fatal("driver released caller's fence", err)
+ }
+ })
+ }
+}
+func TestFilesSeedCommandRefusesBeforeConstruction(t *testing.T) {
+ for _, args := range [][]string{{}, {"repair"}, {"seed", "--preflight"}, {"seed", "--host", "../bad"}, {"seed", "extra"}} {
+ if _, err := parseFilesSeed(args); err == nil {
+ t.Fatal(args)
+ }
+ }
+ host, err := parseFilesSeed([]string{"seed", "--host", recoveryHost})
+ if err != nil || host != recoveryHost {
+ t.Fatal(host, err)
+ }
+ t.Setenv("CARLOS_HOST_CLAIM", "")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "")
+ if err := runSystemFiles(t.Context(), []string{"seed"}, nil); err == nil {
+ t.Fatal("flags off accepted")
+ }
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ if err := runSystemFiles(t.Context(), []string{"seed"}, nil); !errors.Is(err, errFilesSeedDelegation) {
+ t.Fatal("shell reached construction", err)
+ }
+}
+
+func TestFilesSeedRootCapabilityRequiresDelegationValidator(t *testing.T) {
+ f := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"})
+ q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability, Kind: "files-seed"}
+ if _, err := f.root.Serve(t.Context(), f.peer, q); err == nil {
+ t.Fatal("unbound validator accepted")
+ }
+ called := 0
+ f.root.SeedController = func(_ context.Context, p StopPeer) error {
+ called++
+ if p != f.peer {
+ t.Fatal("caller invented peer")
+ }
+ return hostbox.ErrChanged
+ }
+ if _, err := f.root.Serve(t.Context(), f.peer, q); !errors.Is(err, hostbox.ErrChanged) || called != 1 {
+ t.Fatal(err, called)
+ }
+ f.root.SeedController = func(context.Context, StopPeer) error { return nil }
+ if _, err := f.root.Serve(t.Context(), f.peer, q); err != nil {
+ t.Fatal(err)
+ }
+ if len(f.units.starts) != 0 {
+ t.Fatal("capability launched tenant")
+ }
+}
+
+func TestFilesSeedAmbiguousAdmissionRetainsExclusion(t *testing.T) {
+ before := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc"}}}
+ after := hostclaim.Observed{Host: recoveryHost, Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc", WakeRetry: &hostclaim.WakeRetry{State: "attempting"}}}}
+ service := &activator.Service{}
+ token, err := service.AcquireSeedExclusion(t.Context(), recoveryHost, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ runtime := &activator.ClaimRuntime{
+ Read: func(context.Context, string) (hostclaim.Observed, error) { return after, nil },
+ 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 hostclaim.Observed{}, hostbox.ErrChanged
+ },
+ Prepare: func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) {
+ t.Fatal("ambiguous admission prepared")
+ return activator.ClaimStart{}, nil
+ },
+ FailedAdmission: func(context.Context, hostclaim.Observed, error) error { return nil },
+ }
+ _, settled, err := prepareFilesSeed(t.Context(), registry.Route{Host: recoveryHost}, runtime, before, token)
+ if err == nil || settled || token.Check(recoveryHost) != nil {
+ t.Fatal("ambiguous debt reported settled", err, settled)
+ }
+}
+
+func TestFilesSeedExitCodes(t *testing.T) {
+ if filesSeedExitCode(nil) != 0 || filesSeedExitCode(hostbox.ErrChanged) != 1 || filesSeedExitCode(filesSeedIncompleteError{hostbox.ErrChanged}) != 2 {
+ t.Fatal("incorrect status")
+ }
+}
diff --git a/internal/host/claimfilesseed_select.go b/internal/host/claimfilesseed_select.go
index 5c4d2734c..039072395 100644
--- a/internal/host/claimfilesseed_select.go
+++ b/internal/host/claimfilesseed_select.go
@@ -2,10 +2,13 @@ package host
import (
"context"
+ "github.com/carlosframework/platform/internal/activator"
"os"
"path/filepath"
"reflect"
+ "sort"
"strings"
+ "time"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
@@ -235,3 +238,62 @@ func (f *movesFiles) inspectSeedManifests(ctx context.Context, o hostclaim.Obser
}
return found, nil
}
+
+// Claims, including a local claim without a route, define the census. The
+// registry is consulted only after each authoritative observation is read.
+func filesSeedCandidates(ctx context.Context, claims release.Store, box, host string) ([]hostclaim.Observed, error) {
+ if claims == nil || box == "" || ctx == nil {
+ return nil, hostbox.ErrUnsupported
+ }
+ ctx, cancel := context.WithTimeout(ctx, hostbox.RequestLimit)
+ defer cancel()
+ if host != "" {
+ o, err := hostclaim.Read(ctx, claims, host)
+ if err != nil {
+ return nil, err
+ }
+ return []hostclaim.Observed{o}, nil
+ }
+ keys, err := claims.List(ctx, "control/hosts/")
+ if err != nil {
+ return nil, err
+ }
+ var out []hostclaim.Observed
+ for _, key := range keys {
+ tail, ok := strings.CutPrefix(key, "control/hosts/")
+ if !ok {
+ return nil, hostbox.ErrChanged
+ }
+ if strings.Contains(tail, "/") {
+ continue
+ }
+ host := strings.TrimSuffix(tail, ".json")
+ want, err := hostclaim.Key(host)
+ if err != nil || want != key {
+ return nil, hostbox.ErrChanged
+ }
+ o, err := hostclaim.Read(ctx, claims, host)
+ if err != nil {
+ return nil, err
+ }
+ if !o.Absent && o.Claim != nil && o.Claim.Op.Exec == box {
+ out = append(out, o)
+ }
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
+ return out, nil
+}
+
+func selectFilesSeedInputs(ctx context.Context, inputs ClaimWakeInputs, rt registry.Route, selected filesSeedSelection) (ClaimLaunchInputs, error) {
+ if inputs == nil || selected.State != "eligible" {
+ return ClaimLaunchInputs{}, hostbox.ErrUnsupported
+ }
+ in, err := inputs.Select(ctx, rt, selected.Observation, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second})
+ if err != nil {
+ return ClaimLaunchInputs{}, err
+ }
+ if len(in.Roles) != 1 || in.Roles[0] != "main" || in.Executable == "" || len(in.ExecutableSHA256) != 64 || in.DBPath != rt.DBPath || in.Identity.Config == "" || in.Identity.Release == "" || in.Admission.Mode != "cold" || in.Baseline == nil || in.Empty || in.Prepared {
+ return ClaimLaunchInputs{}, hostbox.ErrUnsupported
+ }
+ return in, nil
+}
diff --git a/internal/host/claimfilesseed_select_linux_test.go b/internal/host/claimfilesseed_select_linux_test.go
index 5cece3d53..86aa948c6 100644
--- a/internal/host/claimfilesseed_select_linux_test.go
+++ b/internal/host/claimfilesseed_select_linux_test.go
@@ -202,3 +202,28 @@ func TestFilesSeedManifestCapIncludesTombstonesAndSkips(t *testing.T) {
}
}
}
+
+func TestFilesSeedCandidatesUseClaimsWithoutRegistry(t *testing.T) {
+ f := newFilesSeedFixture(t)
+ got, err := filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "")
+ if err != nil || len(got) != 1 || got[0].ETag != f.observed.ETag {
+ t.Fatal(got, err)
+ }
+ got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", "")
+ if err != nil || len(got) != 0 {
+ t.Fatal(got, err)
+ }
+ got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", filesHost)
+ if err != nil || len(got) != 1 {
+ t.Fatal(got, err)
+ }
+ if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "../bad"); err == nil {
+ t.Fatal("accepted path selector")
+ }
+ if _, err = f.claims.Put(t.Context(), "control/hosts/bad-key", []byte("{}"), ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, ""); err == nil {
+ t.Fatal("ignored malformed census")
+ }
+}
diff --git a/internal/host/claimlaunch_production.go b/internal/host/claimlaunch_production.go
index 041037098..0183c9b99 100644
--- a/internal/host/claimlaunch_production.go
+++ b/internal/host/claimlaunch_production.go
@@ -91,7 +91,7 @@ func claimLaunchFor(root ClaimRootHTTP, actor func(context.Context) (hostbox.Act
ExeRoot: filepath.Join(controlsock.Dir, "exe"),
Room: instancectl.DiskRoom(filepath.Join(roots["data"], "instances")),
}
- launch := &ClaimLaunchRoot{ValidateExecutable: claimStagedExecutable(&instancectl.Server{OptDir: roots["opt"], HostApp: rootInstallationHostApp(coordinator.Guard, coordinator.Evidence)}), DataRoot: roots["data"], Clock: execlease.SystemClock{}, Authenticate: root.Stop.Authenticate, Guard: coordinator.Guard, Actor: actor, Journal: root.Launches, Units: units,
+ launch := &ClaimLaunchRoot{SeedController: verifyFilesSeedDelegation, ValidateExecutable: claimStagedExecutable(&instancectl.Server{OptDir: roots["opt"], HostApp: rootInstallationHostApp(coordinator.Guard, coordinator.Evidence)}), DataRoot: roots["data"], Clock: execlease.SystemClock{}, Authenticate: root.Stop.Authenticate, Guard: coordinator.Guard, Actor: actor, Journal: root.Launches, Units: units,
Kernel: linuxClaimLaunchKernel{OptRoot: roots["opt"], CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"}}
if regionHAEnabled(claimLaunchEnv) {
// Claim Plan 11 "Region HA integration and rollout": the root-owned
diff --git a/internal/host/claimlaunch_root.go b/internal/host/claimlaunch_root.go
index 1d5b20e31..ca0fdef4d 100644
--- a/internal/host/claimlaunch_root.go
+++ b/internal/host/claimlaunch_root.go
@@ -59,6 +59,7 @@ type ClaimLaunchObserved struct {
// roster identity, its local launch journal and its own view of the kernel.
// A request only names which reservation to run.
type ClaimLaunchRoot struct {
+ SeedController func(context.Context, StopPeer) error
executionState rootstate.Store
ValidateExecutable func(host, path, digest string) error
Local hostbox.LocalAuthority
@@ -93,6 +94,15 @@ func (h *ClaimLaunchRoot) Serve(ctx context.Context, peer StopPeer, q hostbox.Ro
out := hostbox.RoleLaunchResponse{V: hostbox.RoleLaunchV, Protocol: hostbox.RoleLaunchProtocol}
switch q.Op {
case hostbox.RoleLaunchCapability:
+ if q.Kind == "files-seed" {
+ if h.SeedController == nil {
+ return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported
+ }
+ if err := h.SeedController(ctx, peer); err != nil {
+ return hostbox.RoleLaunchResponse{}, err
+ }
+ return out, nil
+ }
if q.Kind != "" && q.Kind != hostclaim.KindServing {
if (q.Kind != hostclaim.KindLanding && q.Kind != hostclaim.KindRelocating) || h.Attempts[q.Kind] == nil {
return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported
diff --git a/internal/host/claimservingcollection.go b/internal/host/claimservingcollection.go
index 9585a66f8..745bd08bd 100644
--- a/internal/host/claimservingcollection.go
+++ b/internal/host/claimservingcollection.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"os"
+ "strings"
"github.com/carlosframework/platform/internal/hostbox"
)
@@ -53,6 +54,22 @@ func (l *servingParkLedger) collectClosedLocked(ctx context.Context, r *servingP
}
}
}
+ // Preparations have no authority without their retirement. Remove them
+ // while that record still makes interrupted collection discoverable.
+ names, err := l.state.Names()
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ if strings.HasPrefix(name, "reboot-"+r.Authorization.ID+"-") && strings.HasSuffix(name, ".json") {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if err := l.state.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+ }
// Every record-lock user first takes namespace.lock. No waiter can hold
// this lock's old inode while another process creates its replacement.
// Remove it first: a lost reply leaves the retirement discoverable for a
@@ -63,5 +80,45 @@ func (l *servingParkLedger) collectClosedLocked(ctx context.Context, r *servingP
if err := ctx.Err(); err != nil {
return err
}
- return l.state.Remove("retirement-" + r.Authorization.ID + ".json")
+ if err := l.state.Remove("retirement-" + r.Authorization.ID + ".json"); err != nil {
+ return err
+ }
+ return l.removeCollectedBinding(ctx, r.Authorization.ID)
+}
+
+// Called under namespace.lock. A binding cannot exist before its retirement,
+// and no retirement can be recreated while this lock is held. This removes
+// only private metadata, never an artifact or its namespace binding.
+func (l *servingParkLedger) removeCollectedBinding(ctx context.Context, id string) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if _, err := l.state.Read("retirement-" + id + ".json"); !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ err := l.state.Remove("current-retirement-" + id + ".json")
+ if errors.Is(err, os.ErrNotExist) {
+ return nil
+ }
+ return err
+}
+
+func (l *servingParkLedger) collectOrphanBindings(ctx context.Context) error {
+ names, err := l.state.Names()
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ if !strings.HasPrefix(name, "current-retirement-") || !strings.HasSuffix(name, ".json") {
+ continue
+ }
+ id := strings.TrimSuffix(strings.TrimPrefix(name, "current-retirement-"), ".json")
+ if !stopDigest(id) {
+ return hostbox.ErrChanged
+ }
+ if err := l.removeCollectedBinding(ctx, id); err != nil {
+ return err
+ }
+ }
+ return nil
}
diff --git a/internal/host/claimservingcollection_linux_test.go b/internal/host/claimservingcollection_linux_test.go
index 875b0bfef..fb5b49f5f 100644
--- a/internal/host/claimservingcollection_linux_test.go
+++ b/internal/host/claimservingcollection_linux_test.go
@@ -345,3 +345,71 @@ func TestServingParkCollectionRefusesChangedFilesystem(t *testing.T) {
})
}
}
+
+func TestServingParkCollectionRebootAdjuncts(t *testing.T) {
+ for _, failure := range []string{"none", "preparation-before", "preparation-after", "binding-before", "binding-after"} {
+ t.Run(failure, func(t *testing.T) {
+ f, old, next := servingCollectionFixture(t, false)
+ if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
+ t.Fatal(err)
+ }
+ r, err := f.ledger.read(old.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.now = r.CollectAfter
+ // Collection treats preparation contents as inert private metadata.
+ prep := "reboot-" + old.ID + "-" + servingOtherBoot + ".json"
+ other := "reboot-" + next.ID + "-" + servingOtherBoot + ".json"
+ binding := "current-retirement-" + old.ID + ".json"
+ for _, name := range []string{prep, other} {
+ if err := f.disk.Write(name, []byte("{}")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ fired := false
+ fault := &servingCollectionFaultStore{Store: f.disk}
+ fault.remove = func(name string) error {
+ match := strings.HasPrefix(failure, "preparation") && name == prep || strings.HasPrefix(failure, "binding") && name == binding
+ if match && !fired {
+ fired = true
+ if strings.HasSuffix(failure, "after") {
+ _ = f.disk.Remove(name)
+ }
+ return unix.EIO
+ }
+ return f.disk.Remove(name)
+ }
+ f.ledger.state = fault
+ err = f.ledger.recoverCompleted(t.Context(), old.ID)
+ if failure == "none" {
+ if err != nil {
+ t.Fatal(err)
+ }
+ } else if !fired || !errors.Is(err, unix.EIO) {
+ t.Fatalf("fault not reached: %v", err)
+ }
+ f.ledger.state = f.disk
+ if _, err := f.disk.Read("retirement-" + old.ID + ".json"); err == nil {
+ if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // A lost record-removal reply leaves only a binding; maintenance resumes it.
+ if err := f.disk.Write(binding, []byte("{}")); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.ledger.maintainParks(t.Context(), func() error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range []string{prep, binding, "retirement-" + old.ID + ".json"} {
+ if _, err := f.disk.Read(name); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("adjunct survived: %s %v", name, err)
+ }
+ }
+ if _, err := f.disk.Read(other); err != nil {
+ t.Fatal("collected another retirement's preparation", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/claimservingcollection_rootrunner_linux_test.go b/internal/host/claimservingcollection_rootrunner_linux_test.go
index a4c26d434..8d396abf1 100644
--- a/internal/host/claimservingcollection_rootrunner_linux_test.go
+++ b/internal/host/claimservingcollection_rootrunner_linux_test.go
@@ -62,6 +62,10 @@ func TestRootServingParkCollection(t *testing.T) {
t.Fatal("no replay window", err)
}
f.now = r.CollectAfter
+ prep := "reboot-" + old.ID + "-" + servingOtherBoot + ".json"
+ if err := state.Write(prep, []byte("{}")); err != nil {
+ t.Fatal(err)
+ }
if lost {
fault := &servingCollectionFaultStore{Store: state}
fault.remove = func(name string) error {
@@ -80,6 +84,19 @@ func TestRootServingParkCollection(t *testing.T) {
t.Fatal("collection", err)
}
f.ledger.state = state
+ if _, err := state.Read(prep); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("preparation survived collection", err)
+ }
+ binding := "current-retirement-" + old.ID + ".json"
+ if err := state.Write(binding, []byte("{}")); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.ledger.maintainParks(t.Context(), func() error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := state.Read(binding); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("binding survived maintenance", err)
+ }
for _, suffix := range []string{".json", ".lock"} {
if _, err = state.Read("retirement-" + old.ID + suffix); !errors.Is(err, os.ErrNotExist) {
t.Fatal("collected file survived", suffix, err)
diff --git a/internal/host/claimservingmaintenance.go b/internal/host/claimservingmaintenance.go
index aefaae140..a26fcc34a 100644
--- a/internal/host/claimservingmaintenance.go
+++ b/internal/host/claimservingmaintenance.go
@@ -22,6 +22,9 @@ func (l *servingParkLedger) maintainParks(ctx context.Context, room func() error
if err := ctx.Err(); err != nil {
return err
}
+ if err := l.collectOrphanBindings(ctx); err != nil {
+ return err
+ }
pressure := room()
if pressure != nil && !errors.Is(pressure, instancectl.ErrDiskFull) {
return pressure
diff --git a/internal/host/claimservingpark_rootrunner_linux_test.go b/internal/host/claimservingpark_rootrunner_linux_test.go
index 24e11cfd5..d6a8bb932 100644
--- a/internal/host/claimservingpark_rootrunner_linux_test.go
+++ b/internal/host/claimservingpark_rootrunner_linux_test.go
@@ -680,3 +680,38 @@ func TestRootRebootServingParkAcceptancePhase1(t *testing.T) {
func TestRootRebootServingParkAcceptancePhase2(t *testing.T) {
TestRootRebootServingParkChangedMountPhase2(t)
}
+
+func TestRootFilesSeedRealOriginPreparation(t *testing.T) {
+ servingRootRunner(t)
+ if os.Getenv("PLAN14_ROOT_FIXTURE") == t.Name() {
+ if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
+ t.Fatal(err)
+ }
+ if !plan14RootFixture(t) {
+ t.Fatal("root child did not enter its scratch root")
+ }
+ runFilesSeedOrdinaryPreparation(t, "real-seed", "same-controller", true)
+ return
+ }
+ root := t.TempDir()
+ for _, name := range []string{"scratch", "proc", "foreign-dir"} {
+ if err := os.Mkdir(filepath.Join(root, name), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.WriteFile(filepath.Join(root, "foreign"), nil, 0644); err != nil {
+ t.Fatal(err)
+ }
+ binary, err := os.Executable()
+ if 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}
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("real-root mounted restore: %v\n%s", err, out)
+ } else {
+ t.Log(string(out))
+ }
+}
diff --git a/internal/host/claimservingpeer_rootrunner_linux_test.go b/internal/host/claimservingpeer_rootrunner_linux_test.go
index 87ea29b68..4a00ef143 100644
--- a/internal/host/claimservingpeer_rootrunner_linux_test.go
+++ b/internal/host/claimservingpeer_rootrunner_linux_test.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net"
+ "net/http"
"os"
"os/exec"
"os/user"
@@ -104,6 +105,10 @@ func TestRootServingParkPeerPayload(t *testing.T) {
var reply servingPeerReply
var callErr error
switch q.Action {
+ case "seed-manager":
+ manager, callErr = filesSeedManager(t.Context())
+ case "seed-capability":
+ callErr = NewClaimRoleRootClient(socket).KindCapability(t.Context(), "files-seed")
case "stop":
reply.Stopped, callErr = CollectClaimStop(t.Context(), socket, q.Stop, stopDrainFunc(func(_ context.Context, req StopRequest, fence string) (DrainAcknowledgement, error) {
return DrainClaimAcknowledgement(fence, req.ControllerID, peer.ProcessStart, nil), nil
@@ -336,7 +341,14 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
listener.Close()
t.Fatal(err)
}
- server := &instancectl.Server{ClaimStop: h, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
+ var handler http.Handler = h
+ if namespace == "seed-delegation" {
+ capability := newLaunchFixture(t, hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}).root
+ capability.Authenticate = h.Stop.Authenticate
+ capability.SeedController = verifyFilesSeedDelegation
+ handler = ClaimRootWithLaunch{Root: h, Launch: capability}
+ }
+ server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: t.Logf}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- server.Serve(ctx, listener) }()
@@ -440,6 +452,25 @@ func runServingParkPeerNamespace(t *testing.T, handover, writer bool, namespace
}
return reply
}
+ if namespace == "seed-delegation" {
+ command(servingPeerCommand{Action: "seed-capability"}, true)
+ command(servingPeerCommand{Action: "seed-manager"}, false)
+ command(servingPeerCommand{Action: "seed-capability"}, false)
+ if err := verifyFilesSeedDelegation(t.Context(), first); err == nil {
+ t.Fatal("former controller accepted")
+ }
+ forged := next
+ forged.UID = 0
+ if err := verifyFilesSeedDelegation(t.Context(), forged); err == nil {
+ t.Fatal("root peer accepted")
+ }
+ forged = next
+ forged.ControllerID = "changed"
+ if err := verifyFilesSeedDelegation(t.Context(), forged); err == nil {
+ t.Fatal("changed controller accepted")
+ }
+ return
+ }
q := servingPeerCommand{Action: "park", Current: f.current, Prior: f.prior}
if _, err := (hostbox.ServingParkClient{Call: rootJournalClient(path)}).Park(t.Context(), f.current, f.prior); err == nil {
t.Fatal("root caller authorized retirement")
@@ -648,3 +679,7 @@ func (p servingPeerParkTrace) completeServingDatabasePark(ctx context.Context, c
p.t.Logf("authenticated peer completion authority: %v", err)
return err
}
+
+func TestRootFilesSeedDelegatedController(t *testing.T) {
+ runServingParkPeerNamespace(t, false, false, "seed-delegation")
+}
diff --git a/internal/host/claimservingreboot_commit_linux_test.go b/internal/host/claimservingreboot_commit_linux_test.go
index 0fbdbccd7..ce44f558c 100644
--- a/internal/host/claimservingreboot_commit_linux_test.go
+++ b/internal/host/claimservingreboot_commit_linux_test.go
@@ -7,6 +7,7 @@ import (
"errors"
"os"
"reflect"
+ "strings"
"testing"
"golang.org/x/sys/unix"
@@ -162,3 +163,68 @@ func TestServingParkRebootStartupCommits(t *testing.T) {
t.Fatal(current, err)
}
}
+
+func TestServingParkRebootBindingCollectionCrash(t *testing.T) {
+ f, old := servingRebootPreparationFixture(t, "complete")
+ id := old.Authorization.ID
+ if err := f.p.commitServingReboot(t.Context(), id); err != nil {
+ t.Fatal(err)
+ }
+ current, err := f.p.ledger.read(id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.f.now = current.ReapAfter
+ if err := f.p.ledger.reapLocked(t.Context(), id); err != nil {
+ t.Fatal(err)
+ }
+ current, err = f.p.ledger.read(id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ slot, err := f.p.ledger.slot(current.Authorization)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The fixture advances the slot reference; no payload is deleted by this.
+ slot.ID = ""
+ slot.Pending = ""
+ slot.Last = strings.Repeat("b", 64)
+ collect := func() error {
+ return f.p.ledger.withNamespace(t.Context(), func() error { return f.p.ledger.collectClosedLocked(t.Context(), &current, slot) })
+ }
+ if err := collect(); err != nil {
+ t.Fatal(err)
+ }
+ current, err = f.p.ledger.read(id)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.f.now = current.CollectAfter
+ fault := &servingCollectionFaultStore{Store: f.disk}
+ fault.remove = func(name string) error {
+ if err := f.disk.Remove(name); err != nil {
+ return err
+ }
+ if name == "retirement-"+id+".json" {
+ return unix.EIO
+ }
+ return nil
+ }
+ f.p.ledger.state = fault
+ if err := collect(); !errors.Is(err, unix.EIO) {
+ t.Fatal(err)
+ }
+ f.p.ledger.state = f.disk
+ if err := f.p.ledger.maintainParks(t.Context(), func() error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range []string{"current-retirement-" + id + ".json", servingRebootPreparationName(id, servingOtherBoot)} {
+ if _, err := f.disk.Read(name); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("reboot adjunct survived", name, err)
+ }
+ }
+ if _, err := f.disk.Read("current-namespace.json"); err != nil {
+ t.Fatal("collected namespace binding", err)
+ }
+}
diff --git a/internal/host/claimwakeassembly.go b/internal/host/claimwakeassembly.go
index b9311e491..5bfc8b23d 100644
--- a/internal/host/claimwakeassembly.go
+++ b/internal/host/claimwakeassembly.go
@@ -95,6 +95,7 @@ type claimSyncer interface {
// ClaimWakeAssembly owns cold restore, lease transfer and failure cleanup.
// Root supplies Stop and Launch; moves supplies only its input/publication API.
type ClaimWakeAssembly struct {
+ seedFactory filesSeedRuntimeFactory
ServingPrerequisites func(context.Context, registry.Route, hostclaim.Observed) error
recoveryIntent func(context.Context, hostclaim.Observed, hostclaim.Admission) (hostclaim.Admission, error)
recoveryPrior func(context.Context, hostclaim.Observed) (hostclaim.Observed, bool, error)
@@ -452,7 +453,18 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource
}
return restore.Install(call, in.DBPath, in.Baseline)
}
- s.Materialize = func(ctx context.Context) error { return w.Inputs.Materialize(ctx, o, in, selected) }
+ s.Materialize = func(ctx context.Context) error {
+ // Restore certifies the admitted root and advances the claim ETag.
+ // C8 proves origins against the fresh observation of this writer.
+ fresh, err := read(ctx, o.Host)
+ if err != nil {
+ return err
+ }
+ if !hostclaim.SameWriterIdentity(o, fresh) || !a.advance(fresh, w.Continue) {
+ return hostbox.ErrChanged
+ }
+ return w.Inputs.Materialize(ctx, fresh, in, selected)
+ }
s.Build = func() *exec.Cmd {
cmd := exec.Command(in.Executable, in.Args...)
cmd.Env = append([]string(nil), in.Env...)
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 64c50a42a..444f01960 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -753,7 +753,7 @@ var plan9B8CensusInventory = []censusEntry{
{"internal/host/bootstrap.go", "const tmpfilesConf", "key-site", "lit:instances", "71d83f240cb1c3ab6f9dd2954b1f3e93b90133362ec12193746f60e75aeced7d"},
{"internal/host/bootstrap.go", "bootstrapWith", "key-site", "lit:instances", "36ff18685872be8adfbf02d05676990bf9097c0fcc9695d3cae2f905ec59665c"},
// Region HA stage B: installs root's relocating attempt reader behind CARLOS_REGION_HA; names the instances root for the launch, writes nothing.
- {"internal/host/claimlaunch_production.go", "claimLaunchFor", "key-site", "lit:instances", "a283c8ff26452e19323611ae1fbd4ad9a51265f186afdb1ff99de089619e6f9c"},
+ {"internal/host/claimlaunch_production.go", "claimLaunchFor", "key-site", "lit:instances", "b6c4f60ed5783496e89c45c9c612cbd8758b6a05c19a706c5eb5640dfea2505d"},
{"internal/host/claimretire_transport.go", "retirementAcknowledgement", "nonrecord", "sel:Put", "5c09795fbe9b952436d16bcd58e846ac27e9dbcf959fcf1cd6c432af0f3ee416"},
// D1 compares the retained registration's record key to the fresh claim; no record write.
{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey", "107989f561409fd8c11c2666d0672b8dbeb368842fe00ad6a7b84fece2150438"},
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index 01768347c..de7ec3c7c 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,17 +1,57 @@
-# Files seed run 23 overlay; pending Paul’s census ruling.
-# Serving retirement is bound; private seed command and integrated acceptance remain unfinished.
-# Loaded LAST after main through hostfiles-cap-236; no legacy fingerprints changed.
-# Every classification is rev3=PROPOSED-seed.
+# Files seed overlay; loads LAST. Run 24 proposals, not census approval.
+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 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/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
+internal/activator/seed_diagnostic.go Service.allowFilesDiagnostic 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 1563717f502a272d0c52099710343c02df92f878ca387642677f16485f881233
+internal/activator/seed_diagnostic.go Service.allowFilesDiagnosticLocked 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 98bc2d31cea03444e6afd592b66b3cd60f256466bb0ca758ea98c8b6f51beccf
+internal/activator/seed_diagnostic.go ServingFilesMissing.Error 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 218c638954420be24d61341d3202ce244584e4e0ec2e2ea266b3a8a44e679151
+internal/activator/seed_diagnostic.go ServingFilesMissing.Unwrap 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 2dbcde2a6f4e6b26eb5e5aa85e3299a07889b31025ee691f3421cd6db955269b
+internal/activator/seed_diagnostic.go type ServingFilesMissing 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 48e425db3cb6719ee3f0661eae049769acee82087bd1ca28899327691670dcf8
+internal/activator/seed_diagnostic.go type seedDiagnostic 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 77e518b943d2edc7ad6c9da9afd63d0ae733fab94beae54c35fa021293bac8a8
+internal/activator/seed_exclusion.go SeedExclusion.Check 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 d44346bcb333b8d6eae4d4021123edbf036cbc3289add8e35f5d3cc90eafa2fc
+internal/activator/seed_exclusion.go SeedExclusion.Release 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 bb43b6ed75ceff5b3d5d0365b42e609be6f31595ba2933502c24ef840e0253b4
+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/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/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.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 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 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 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; safe nested layout and bounded manifest metadata; evidence absence refuses before retries; only genuine empty authority can omit instance directory file:Read@600,whole:Route@548 43fc71ee9a589203b1889c3432421638d5f61dc5741610c17376d4129ff55619
+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_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 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
internal/host/claimfilesseed_select.go filesSeedChain 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 be995b5de271102602ff47d5c532ed226502dd364264fa688673ee7e7ad16493
internal/host/claimfilesseed_select.go inspectFilesSeed 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 file:Exec@1186,file:Read@730,registry:Account@1618,registry:App@1658,registry:Backing@1492,registry:Host@1331,registry:Replica@1413,registry:Seed@1475,registry:Tunnel@1547,registry:Unit@1512,whole:Route@1318,whole:Route@1763 37a068572b9069edf0b2676a960897e40d27e193a516276efcfa252044caaa70
internal/host/claimfilesseed_select.go movesFiles.inspectSeedManifests 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 b69e3c79814750c29a6989feb1283143c4af40f80e2c2da0e876237c2381c472
+internal/host/claimfilesseed_select.go selectFilesSeedInputs 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 registry:DBPath@552,whole:Route@288 e3e32f864c45b48889652d44535c12618098d706e53acc10cfc23014010ad43e
internal/host/claimfilesseed_select.go type filesSeedSelection 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 c4229397b9ff57b5326381b5052b8cba752671a4d80a686c191b3517490dba69
internal/host/claimfilesseed_stage.go filesSeedCheckingStore.Put 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:ReadAll@794 1b9671f8b7b441b98087fe762ad54013ead435faa8eb62a930cc0ff207e897b0
internal/host/claimfilesseed_stage.go filesSeedOperation.admitted 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 whole:Route@770 a4f97a70ab72756c46ec3dbda713b3a6f1a105774ad00c2f6e7a87754ba98e3d
@@ -23,6 +63,9 @@ internal/host/claimfilesseed_stage.go type filesSeedOperation authority C,J4 rev
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 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
@@ -32,7 +75,9 @@ internal/host/claimservingartifact_other.go checkServingArtifactAbsence authorit
internal/host/claimservingboot_linux.go servingParkBootID authority C,J4 rev3=PROPOSED-seed; bounded kernel boot-ID read from /proc; no caller-supplied path or identity file:Open@54,file:ReadAll@162 af8e5641ef60fcd307dec013df6eba3e50a29d2cb811c5b2cddf10c5c9404e30
internal/host/claimservingboot_linux.go servingParkFS.checkBoot authority C,J4 rev3=PROPOSED-seed; private namespace boot identity compared with the current kernel boot; missing old identity or a reboot refuses without refreshing mount numbers 3d380a2fa685f3be883f9c595a01ee437d51597a1b05ab09fb9c50dfead2cac1
internal/host/claimservingboot_linux.go servingParkFS.currentBoot authority C,J4 rev3=PROPOSED-seed; production boot reader and canonical nonzero UUID validation; missing or unreadable boot evidence refuses f6d671a41210250791f421efcf168fa98e3c2b4bd8b6aa314e9664261fd2b034
-internal/host/claimservingcollection.go servingParkLedger.collectClosedLocked authority C,J4 rev3=PROPOSED-seed; identity-checked closed record and slot; legacy collection marker writes through inverse historical projection 5176a798a8f6258f3284c67334ec9812788f2c24a5143060350cd9efd89345cb
+internal/host/claimservingcollection.go servingParkLedger.collectClosedLocked 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 2e9b6705b8d97c0b66ef12de412be00f81c0a5d844ca16fcd68bc0d541429266
+internal/host/claimservingcollection.go servingParkLedger.collectOrphanBindings 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 59a0e9fbfe1149f6596af7eaedb1e6df4dfbe6a55f1c192aba232a6ec63f84d1
+internal/host/claimservingcollection.go servingParkLedger.removeCollectedBinding 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 file:Read@156 29d5911c470a15fa0812ed4fba2475d5a6cb6841b6b03e05f9e1e539d5fea4d5
internal/host/claimservingdisplaced_linux.go const servingParkRootSearchLimit authority C,J4 rev3=PROPOSED-seed; fixed bound of 128 immediate parent entries; overflow refuses before opening candidate contents or deleting artifacts f7767fc1ea8f855aa6ed5e145e5b576c061c374876b57291f7182ebbdd899595
internal/host/claimservingdisplaced_linux.go servingParkFS.displacedRoot authority C,J4 rev3=PROPOSED-seed; completed private quarantine and prior namespace identity; no-follow bounded parent listing/statx and exact root-owned directory open; same device/inode/mount and safe parent recheck; no unknown child traversal file:Fstat@649,file:Read@144,file:Readdirnames@939,file:Statx@1259 762661ee74aaed30ca1147160a995786ad3a55200f6ffd25106125799ff0c74d
internal/host/claimservingdisplaced_linux.go servingParkFS.eventRoot authority C,J4 rev3=PROPOSED-seed; current namespace and recorded retirement root; selects only current or independently recorded prior namespace; ambiguous old absent-event records refuse file:Read@562 7c47b53f88059eea930d483465570fc7205b3cec27a118472071ca4d6df7d5e7
@@ -63,7 +108,7 @@ 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; root park records, slot ownership and deadlines; bounded candidate set, oldest complete parks, verified deletion under pressure or expiry; cancelled on shutdown ed7abaaf843199235a186f3de9698cf54143584732a5d2f36876f53e55438cb7
+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_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
@@ -206,7 +251,8 @@ internal/host/claimservingtopology_linux.go type servingParkTopology authority C
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,G,J1,J4 rev3=PROPOSED-seed; serving metadata/layout prerequisite before retry settlement and lease acquisition; existing launch authority unchanged file:Command@9968,file:Prepare@7029,file:Read@1348,file:Read@1442,registry:Backing@2672,registry:Host@12582,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@10106,whole:Route@14302,whole:Route@14374,whole:Route@2899,whole:Route@5830 1ecfe167224d33fb7dcf12f2db34e86e542b324b912436da98b2b0e46166d684
+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/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
@@ -244,8 +290,11 @@ internal/hostguard/coldhandover.go Guard.BeginServingHandover authority C,J4 rev
internal/hostguard/coldhandover.go Guard.beginColdHandover authority C,J4 rev3=PROPOSED-seed; fresh claim and root installation under host locks; exact completed stop for admitted predecessors, validated no-process path for session zero, and durable successor barrier 6bdeec90f4322aa0824557cd35950af428b25889e15badeb4b017ccbbf0aae70
internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108
internal/hostguard/writerstop.go Guard.CompletedServingStops authority C rev3=PROPOSED-seed; root fence journal through the existing Guard backend; exact host, incarnation, stamp, session and box, with empty operation, phase and obligation; no mixed-session or pending-only evidence 79260151b0fe4f86c59cbe65d727cdceb56028bdfdb208fc9f02cebba1473a98
-@coverage internal/activator 5ecc6e99362bbd2c071607da2a247d01018d5380858d53dc9fdb9a88523200f4
-@coverage internal/host 18a469804421746da4e1057cb05e58db6c0606183a7557c0d603ba1a5967b1ab
+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/hostfiles a38dbcd9789ba4acd237e88c834f2b2bfca9847b0d43c32b0116dc8996d7a142
@coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6
diff --git a/system.go b/system.go
index 03fb153a8..761e5bc12 100644
--- a/system.go
+++ b/system.go
@@ -49,7 +49,7 @@ func systemVerb(args []string) (verb string, rest []string, component string) {
// untouched in rest for runSystemAntivirusVerb to read.
// layout is a family too: migrate|revert|status stay in rest for
// runSystemLayout.
- case "guard-arm", "guard-exec", "journal-migrate", "floor-allows-prev", "update", "status", "rollback", "cancel-activation", "apply", "antivirus", "layout", "geoip", "seeds-floor", "legacy-routes", "help", "-h", "--help":
+ case "files", "guard-arm", "guard-exec", "journal-migrate", "floor-allows-prev", "update", "status", "rollback", "cancel-activation", "apply", "antivirus", "layout", "geoip", "seeds-floor", "legacy-routes", "help", "-h", "--help":
return args[0], args[1:], component
}
return "", args, component
@@ -58,6 +58,8 @@ func systemVerb(args []string) (verb string, rest []string, component string) {
func runSystem(args []string) {
verb, rest, _ := systemVerb(args)
switch verb {
+ case "files":
+ host.RunSystemFiles(rest)
case "guard-arm":
host.RunSystemGuardArm(rest)
case "guard-exec":
diff --git a/system_test.go b/system_test.go
index a9af9f119..e9569f172 100644
--- a/system_test.go
+++ b/system_test.go
@@ -23,6 +23,7 @@ func TestSystemVerbAcceptsTheComponentForm(t *testing.T) {
{[]string{"antivirus", "status"}, "antivirus", "router", 1},
{[]string{"antivirus", "enable", "--console", "x"}, "antivirus", "router", 3},
{[]string{"antivirus"}, "antivirus", "router", 0},
+ {[]string{"files", "seed", "--host", "b.example"}, "files", "router", 3},
// layout is a family too: its sub-verb and flags stay in rest.
{[]string{"layout", "migrate", "--box", "x"}, "layout", "router", 3},
{[]string{"layout", "revert", "--force", "--yes"}, "layout", "router", 3},