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>
36 files changed,
+1992
−21
approved_moves_copy_test.go+1 −0internal/activator/activator.go+9 −3internal/activator/maintenance.go+6 −2internal/activator/seed_diagnostic.go+51 −0internal/activator/seed_diagnostic_test.go+48 −0internal/activator/seed_exclusion.go+96 −0internal/activator/seed_exclusion_test.go+93 −0internal/activator/swaphold.go+3 −0internal/host/claimassembly.go+6 −0internal/host/claimfilesseed.go+266 −0internal/host/claimfilesseed_command.go+167 −0internal/host/claimfilesseed_delegation_linux.go+132 −0internal/host/claimfilesseed_delegation_other.go+13 −0internal/host/claimfilesseed_guard.go+5 −0internal/host/claimfilesseed_integration_linux_test.go+171 −0internal/host/claimfilesseed_restore_linux_test.go+18 −0internal/host/claimfilesseed_retry.go+56 −0internal/host/claimfilesseed_retry_linux_test.go+124 −0internal/host/claimfilesseed_runtime.go+114 −0internal/host/claimfilesseed_runtime_test.go+155 −0internal/host/claimfilesseed_select.go+62 −0internal/host/claimfilesseed_select_linux_test.go+25 −0internal/host/claimlaunch_production.go+1 −1internal/host/claimlaunch_root.go+10 −0internal/host/claimservingcollection.go+58 −1internal/host/claimservingcollection_linux_test.go+68 −0internal/host/claimservingcollection_rootrunner_linux_test.go+17 −0internal/host/claimservingmaintenance.go+3 −0internal/host/claimservingpark_rootrunner_linux_test.go+35 −0internal/host/claimservingpeer_rootrunner_linux_test.go+36 −1internal/host/claimservingreboot_commit_linux_test.go+66 −0internal/host/claimwakeassembly.go+13 −1internal/provision/plan9_b8_census_test.go+1 −1internal/provision/testdata/plan14-root-reads-seed.tsv+59 −10system.go+3 −1system_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(), ¤t, 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}, |