carlos / platform Public

WIP claim Plan 14 track E checkpoint 15 (not for merge): census-backed root restart consumption wired into both adoption entry points

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev d5459b2a69928e49a4ca856da18b7ba7a4196018 parent ab75e30
8 files changed, +1164 −13
  • internal/host/agent.go +16 −10
  • internal/host/plan14_restart_consumption_process_test.go +184 −0
  • internal/host/plan14_restart_consumption_test.go +550 −0
  • internal/host/rootrestart_requests.go +237 −0
  • internal/host/rootrestart_requests_source.go +130 −0
  • internal/provision/plan14_e_write_census_test.go +3 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +14 −3
  • internal/release/rootrestart_history.go +30 −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	335.390s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.192s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.238s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.382s
ok  	github.com/carlosframework/platform/internal/activator	227.539s
ok  	github.com/carlosframework/platform/internal/agentsock	0.014s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.297s
ok  	github.com/carlosframework/platform/internal/apperr	0.073s
ok  	github.com/carlosframework/platform/internal/applog	0.129s
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.521s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.691s
ok  	github.com/carlosframework/platform/internal/changelog	0.292s
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	363.557s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.646s
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.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.299s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.007s
ok  	github.com/carlosframework/platform/internal/edge	26.092s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.157s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.039s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.123s
ok  	github.com/carlosframework/platform/internal/host	100.094s
ok  	github.com/carlosframework/platform/internal/hostactors	0.275s
ok  	github.com/carlosframework/platform/internal/hostbox	2.126s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.492s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	4.903s
ok  	github.com/carlosframework/platform/internal/hostfence	0.032s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.563s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.005s
ok  	github.com/carlosframework/platform/internal/hostops	1.425s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.014s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.439s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.129s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.015s
ok  	github.com/carlosframework/platform/internal/infraview	0.014s
ok  	github.com/carlosframework/platform/internal/instancectl	1.815s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.718s
ok  	github.com/carlosframework/platform/internal/instancefs	0.019s
ok  	github.com/carlosframework/platform/internal/instancekey	0.017s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.424s
ok  	github.com/carlosframework/platform/internal/lease	9.466s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.019s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.160s
ok  	github.com/carlosframework/platform/internal/maintenance	0.089s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.546s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.077s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	20.955s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.771s
ok  	github.com/carlosframework/platform/internal/objstore	0.170s
ok  	github.com/carlosframework/platform/internal/ops	0.163s
ok  	github.com/carlosframework/platform/internal/park	0.204s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	13.279s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.046s
ok  	github.com/carlosframework/platform/internal/relay	7.228s
ok  	github.com/carlosframework/platform/internal/release	20.414s
ok  	github.com/carlosframework/platform/internal/rollout	3.747s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.008s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.278s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.582s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.548s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.218s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.551s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/13 23:40:03 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE3B35C84D20A5, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 05565a072..6ba845156 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4413,11 +4413,14 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// parsed below and is fatal when unusable. A restart request must not fail
// on a key it never needs.
if *restartOnly {
- routes, err := reg.List()
- if err != nil {
- log.Fatalf("adopt: list routes: %v", err)
- }
- restartPass(ctx, routes, store, restartMarksPath(dataDir()), restart)
+ logRestartRequestError(productionRestartRequestPass(ctx, store, claims.Writes, func() error {
+ routes, err := reg.List()
+ if err != nil {
+ log.Fatalf("adopt: list routes: %v", err)
+ }
+ restartPass(ctx, routes, store, restartMarksPath(dataDir()), restart)
+ return nil
+ }))
return nil
}
// Parsed here, before config sync, exactly as the full pass always has
@@ -4463,11 +4466,14 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// with an unstageable release would otherwise never reach this line — and
// a wedged instance is most worth cycling precisely on a box where
// something else is already wrong.
- if routes, err := reg.List(); err != nil {
- log.Printf("adopt: list routes for the restart pass: %v", err)
- } else {
- restartPass(ctx, routes, store, restartMarksPath(dataDir()), restart)
- }
+ logRestartRequestError(productionRestartRequestPass(ctx, store, claims.Writes, func() error {
+ if routes, err := reg.List(); err != nil {
+ log.Printf("adopt: list routes for the restart pass: %v", err)
+ } else {
+ restartPass(ctx, routes, store, restartMarksPath(dataDir()), restart)
+ }
+ return nil
+ }))
// Step 2 (spec §4d): the grant-drift capability converge, BETWEEN config
// sync and release adoption. Before release.Pass because it repairs the
diff --git a/internal/host/plan14_restart_consumption_process_test.go b/internal/host/plan14_restart_consumption_process_test.go
new file mode 100644
index 000000000..cc0b0c7f5
--- /dev/null
+++ b/internal/host/plan14_restart_consumption_process_test.go
@@ -0,0 +1,184 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+)
+
+type restartRequestProcessFixture struct {
+ Unit rollbackProcessFixture
+ Claim *hostclaim.Claim
+ Installations, Journal string
+}
+
+func TestPlan14RootRestartProductionCrash(t *testing.T) {
+ if boundary := os.Getenv("PLAN14_REQUEST_CRASH"); boundary != "" {
+ dir := os.Getenv("PLAN14_REQUEST_DIR")
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ storeDir := filepath.Join(dir, "deployment")
+ store := release.NewDirStore(storeDir)
+ keys, err := f.e.store.List(t.Context(), "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, key := range keys {
+ raw, _, err := f.e.store.Get(t.Context(), key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Put(t.Context(), key, raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ crash := func() {
+ state := restartRequestProcessFixture{Unit: rollbackProcessFixture{Props: f.u.props, Exe: f.u.exe, Opt: f.u.opt, Bake: f.e.bake, Store: storeDir, Effects: f.u.effects}, Claim: f.claim,
+ Installations: f.p.source.state.(*rollbackDisk).dir, Journal: f.p.journal.Store.(*rollbackDisk).dir}
+ raw, err := json.Marshal(state)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "fixture.json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ os.Exit(74)
+ }
+ disk := f.p.journal.Store.(*rollbackDisk)
+ classify := func(raw []byte) string {
+ var r struct {
+ Pending *release.RestartIntent
+ ScopeID string `json:"scope_id"`
+ }
+ if json.Unmarshal(raw, &r) != nil {
+ return ""
+ }
+ if r.Pending == nil {
+ if r.ScopeID != release.RestartScopeNone {
+ return "consumed"
+ }
+ return "baseline"
+ }
+ if r.Pending.Targets[0].After != "" {
+ return "completed"
+ }
+ if r.Pending.Targets[0].Before != "" {
+ return "intent"
+ }
+ return "pending"
+ }
+ disk.afterWrite = func(_ string, raw []byte) {
+ if classify(raw) == boundary {
+ crash()
+ }
+ }
+ if boundary == "intent-visible" {
+ disk.beforeWrite = func(name string, raw []byte) {
+ if classify(raw) != "intent" {
+ return
+ }
+ file, err := os.CreateTemp(disk.dir, "visible-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = file.Write(raw); err != nil {
+ t.Fatal(err)
+ }
+ if err = file.Sync(); err != nil {
+ t.Fatal(err)
+ }
+ if err = file.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err = os.Rename(file.Name(), filepath.Join(disk.dir, name)); err != nil {
+ t.Fatal(err)
+ }
+ // Exit before syncing the parent of the visible rename.
+ crash()
+ }
+ }
+ f.u.afterEffect = func() {
+ if boundary == "effect" {
+ crash()
+ }
+ }
+ _ = f.run(t)
+ t.Fatal("crash boundary not reached")
+ }
+ for _, boundary := range []string{"pending", "intent", "intent-visible", "effect", "completed", "consumed"} {
+ t.Run(boundary, func(t *testing.T) {
+ dir := t.TempDir()
+ ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
+ defer cancel()
+ cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestPlan14RootRestartProductionCrash$", "-test.count=1")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "TMPDIR="+dir, "PLAN14_REQUEST_DIR="+dir, "PLAN14_REQUEST_CRASH="+boundary, "CARLOS_HOST_CLAIM=")
+ out, err := cmd.CombinedOutput()
+ var exit *exec.ExitError
+ if !errors.As(err, &exit) || exit.ExitCode() != 74 {
+ t.Fatalf("child: %v\n%s", err, out)
+ }
+ raw, err := os.ReadFile(filepath.Join(dir, "fixture.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var saved restartRequestProcessFixture
+ if err = json.Unmarshal(raw, &saved); err != nil {
+ t.Fatal(err)
+ }
+ u := &plan14RestartUnits{props: saved.Unit.Props, exe: saved.Unit.Exe, opt: saved.Unit.Opt, effects: saved.Unit.Effects}
+ store := release.NewDirStore(saved.Unit.Store)
+ state := &rollbackDisk{dir: saved.Installations}
+ guard := hostguard.Guard{DataDir: t.TempDir(), LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(state), Claims: rootViewClaimReader{obs: hostclaim.Observed{Host: observedHost, ETag: "claim", Claim: saved.Claim}}}
+ source := &proofBindingSource{store: store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}, healthy: func(acct, app, version, digest string) *release.HealthyBuild {
+ for _, b := range release.ReadHealthy(saved.Unit.Bake, acct, app) {
+ if b.Version == version && b.SHA256 == digest {
+ return &b
+ }
+ }
+ return nil
+ }}
+ now, sleep := settleClock()
+ p := rootRestartRequests{journal: &release.RootRestarts{Store: &rollbackDisk{dir: saved.Journal}}, source: rootRestartRequestSource{state: state, store: store, census: func() (rootparticipation.Census, error) {
+ return rootparticipation.Census{Box: "box", Enrollment: "en", Migration: 1, Complete: true}, nil
+ }},
+ sys: u, optDir: u.opt, getenv: func(string) string { return "5s" }, checks: rootRestartChecks{host: source.forHost, ready: func(registry.Route) func() error { return func() error { return nil } }, now: now, sleep: sleep},
+ writes: hostwrite.Config{Enabled: true, Selector: hostbox.WorkSource{Guard: guard, Box: "box", Region: "ie", Now: time.Now, Policy: func(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
+ return hostbox.Serving(c, s)
+ }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }}}}
+ err = p.run(t.Context())
+ if boundary == "intent" || boundary == "intent-visible" {
+ if err == nil || len(u.effects) != 0 {
+ t.Fatalf("ambiguous replay: %v %v", err, u.effects)
+ }
+ u.props.InvocationID += "reconciled"
+ u.props.MainPID++
+ if err = p.run(t.Context()); err != nil || len(u.effects) != 0 {
+ t.Fatalf("reconcile: %v %v", err, u.effects)
+ }
+ } else if err != nil || len(u.effects) != 1 {
+ t.Fatalf("resume: %v %v", err, u.effects)
+ }
+ count := len(u.effects)
+ if err = p.run(t.Context()); err != nil || len(u.effects) != count {
+ t.Fatalf("replay: %v %v", err, u.effects)
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_restart_consumption_test.go b/internal/host/plan14_restart_consumption_test.go
new file mode 100644
index 000000000..993549068
--- /dev/null
+++ b/internal/host/plan14_restart_consumption_test.go
@@ -0,0 +1,550 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+)
+
+type restartConsumptionFixture struct {
+ p rootRestartRequests
+ e *proofRootEnv
+ u *plan14RestartUnits
+ claim *hostclaim.Claim
+ marks string
+}
+
+func newRestartConsumptionFixture(t *testing.T) restartConsumptionFixture {
+ t.Helper()
+ e, rt, u, checks, _ := plan14AdoptionFixture(t)
+ rec, err := provision.GetRecord(t.Context(), e.store, rt.Account, rt.App, rt.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec.Environment = "relay"
+ rec.CreatedAt = time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
+ plan14PutRecord(t, e.store, *rec)
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := &hostclaim.Claim{Host: rt.Host, Holder: &hostclaim.Holder{Key: provision.RecordKey(rt.Account, rt.App, rt.Host), Account: rt.Account, App: rt.App, Incarnation: inc, Stamp: 1, Session: 1, WriterBox: "box"}}
+ c.Op.Kind, c.Op.Exec = hostclaim.KindServing, "box"
+ state := &rollbackDisk{dir: t.TempDir()}
+ g := hostguard.Guard{DataDir: t.TempDir(), LockDir: t.TempDir(), Claims: rootViewClaimReader{obs: hostclaim.Observed{Host: rt.Host, ETag: "claim", Claim: c}}, Root: hostguard.NewRootAuthority(state)}
+ in := hostguard.RootInstallation{Host: rt.Host, Incarnation: inc, Stamp: 1, Account: rt.Account, App: rt.App, Record: c.Holder.Key, Box: "box", Enrollment: "en", Migration: 1, InventoryRevision: 1}
+ if err := g.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ writes := hostwrite.Config{Enabled: true, Selector: hostbox.WorkSource{Guard: g, Box: "box", Region: "ie", Now: time.Now,
+ Policy: func(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
+ return hostbox.Serving(c, s)
+ },
+ Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil },
+ }}
+ p := rootRestartRequests{journal: &release.RootRestarts{Store: &rollbackDisk{dir: t.TempDir()}},
+ source: rootRestartRequestSource{state: state, store: e.store, census: func() (rootparticipation.Census, error) {
+ return rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, Complete: true}, nil
+ }},
+ writes: writes, sys: u, checks: checks, getenv: func(string) string { return "5s" }, optDir: u.opt,
+ }
+ return restartConsumptionFixture{p: p, e: e, u: u, claim: c, marks: restartMarksPath(t.TempDir())}
+}
+
+func (f restartConsumptionFixture) request(t *testing.T, kind, value string) {
+ t.Helper()
+ if _, err := release.AppendRestartScope(t.Context(), f.e.store, "bab", "console", release.RestartScope{Kind: kind, Value: value}); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func (f restartConsumptionFixture) run(t *testing.T) error {
+ t.Helper()
+ return restartRequestPass(func() (bool, error) { return false, nil }, func() error {
+ routes, err := f.e.root.reg.List()
+ if err != nil {
+ return err
+ }
+ restartPass(t.Context(), routes, f.e.store, f.marks, func(host string) error { return f.u.TryRestart("console.service") })
+ return nil
+ }, func() error { return f.p.run(t.Context()) })
+}
+
+func TestPlan14RootRestartProductionConsumption(t *testing.T) {
+ for _, attack := range []string{"none", "delete-marks", "rename-parent", "forge-marks", "delete-row", "forge-selector", "forge-unit"} {
+ t.Run(attack, func(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeEnvironment, "relay")
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("first request: %v %v", err, f.u.effects)
+ }
+ if err := os.WriteFile(f.marks, []byte(`{"schema":1,"apps":{},"scopes":{}}`), 0600); err != nil {
+ t.Fatal(err)
+ }
+ switch attack {
+ case "delete-marks":
+ if err := os.Remove(f.marks); err != nil {
+ t.Fatal(err)
+ }
+ case "rename-parent":
+ if err := os.Rename(filepath.Dir(f.marks), filepath.Dir(f.marks)+".old"); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = os.RemoveAll(filepath.Dir(f.marks) + ".old") })
+ case "forge-marks":
+ if err := os.WriteFile(f.marks, []byte(`{"schema":1,"apps":{"bab|console":"forged"},"scopes":{"bab|console":"forged"}}`), 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "delete-row":
+ if err := f.e.root.reg.Delete(observedHost); err != nil {
+ t.Fatal(err)
+ }
+ case "forge-selector", "forge-unit":
+ r, err := f.e.root.reg.Get(observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Environment, r.Unit, r.Hibernate = "forged", "unrelated.service", true
+ if err := f.e.root.reg.Upsert(*r); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("replay: %v %v", err, f.u.effects)
+ }
+ f.request(t, release.RestartScopeEnvironment, "forged")
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("widened: %v %v", err, f.u.effects)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ if err := f.run(t); err != nil || len(f.u.effects) != 2 {
+ t.Fatalf("fresh request: %v %v", err, f.u.effects)
+ }
+ })
+ }
+}
+
+func TestPlan14RootRestartProductionHeldRollover(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.claim.Op.Kind = hostclaim.KindLanding
+ f.request(t, release.RestartScopeHost, observedHost)
+ if err := f.e.root.reg.Delete(observedHost); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("held: %v %v", err, f.u.effects)
+ }
+ for i := 0; i < 34; i++ {
+ f.request(t, release.RestartScopeHost, "other.example.com")
+ }
+ f.claim.Op.Kind = hostclaim.KindServing
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("lost pending: %v %v", err, f.u.effects)
+ }
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("replayed pending: %v %v", err, f.u.effects)
+ }
+}
+
+func TestPlan14RootRestartProductionBindingChange(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.claim.Op.Kind = hostclaim.KindLanding
+ f.request(t, release.RestartScopeHost, observedHost)
+ _ = f.run(t)
+ rec, err := provision.GetRecord(t.Context(), f.e.store, "bab", "console", observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec.Environment = "replacement"
+ plan14PutRecord(t, f.e.store, *rec)
+ f.claim.Op.Kind = hostclaim.KindServing
+ if err := f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("changed binding: %v %v", err, f.u.effects)
+ }
+}
+
+func TestPlan14RootRestartProductionAdmissionAfterIntent(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ disk := f.p.journal.Store.(*rollbackDisk)
+ disk.afterWrite = func(_ string, raw []byte) {
+ var r struct{ Pending *release.RestartIntent }
+ if json.Unmarshal(raw, &r) == nil && r.Pending != nil && r.Pending.Targets[0].Before != "" {
+ f.claim.Op.Kind = hostclaim.KindLanding
+ }
+ }
+ if err := f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("move crossed dispatch intent: %v %v", err, f.u.effects)
+ }
+ disk.afterWrite = nil
+ f.claim.Op.Kind = hostclaim.KindServing
+ if err := f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("ambiguous intent replayed: %v %v", err, f.u.effects)
+ }
+ // A verified successor completes the captured request without another call.
+ f.u.props.InvocationID += "successor"
+ f.u.props.MainPID++
+ if err := f.run(t); err != nil || len(f.u.effects) != 0 {
+ t.Fatalf("successor: %v %v", err, f.u.effects)
+ }
+}
+
+func TestPlan14RootRestartProductionUnknown(t *testing.T) {
+ for _, attack := range []string{"census-error", "census-incomplete", "root-corrupt", "record-missing", "retired-pending", "flag-off", "foreign-census", "inactive"} {
+ t.Run(attack, func(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ switch attack {
+ case "census-error":
+ f.p.source.census = func() (rootparticipation.Census, error) { return rootparticipation.Census{}, os.ErrPermission }
+ case "census-incomplete":
+ f.p.source.census = func() (rootparticipation.Census, error) { return rootparticipation.Census{Box: "box"}, nil }
+ case "foreign-census":
+ f.p.source.census = func() (rootparticipation.Census, error) {
+ return rootparticipation.Census{Box: "other", Complete: true}, nil
+ }
+ case "root-corrupt":
+ if err := f.p.source.state.Write(observedHost+".json", []byte(`{}`)); err != nil {
+ t.Fatal(err)
+ }
+ case "record-missing":
+ if err := f.e.store.Delete(t.Context(), provision.RecordKey("bab", "console", observedHost)); err != nil {
+ t.Fatal(err)
+ }
+ case "retired-pending":
+ f.claim.Op.Kind = hostclaim.KindLanding
+ _ = f.run(t)
+ f.claim.Op.Kind = hostclaim.KindServing
+ if err := f.p.source.state.Remove(observedHost + ".json"); err != nil {
+ t.Fatal(err)
+ }
+ case "flag-off":
+ f.p.writes.Enabled = false
+ case "inactive":
+ f.u.props.ActiveState = "inactive"
+ }
+ if err := f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("unknown granted: %v %v", err, f.u.effects)
+ }
+ })
+ }
+}
+
+func TestPlan14RootRestartRequestCensusGate(t *testing.T) {
+ for _, mode := range []string{"absent", "present", "unreadable"} {
+ t.Run(mode, func(t *testing.T) {
+ old, root := 0, 0
+ err := restartRequestPass(func() (bool, error) {
+ if mode == "unreadable" {
+ return false, os.ErrPermission
+ }
+ return mode == "absent", nil
+ }, func() error { old++; return nil }, func() error { root++; return nil })
+ if mode == "unreadable" {
+ if err == nil || old+root != 0 {
+ t.Fatal(err, old, root)
+ }
+ return
+ }
+ if err != nil || (mode == "absent" && (old != 1 || root != 0)) || (mode == "present" && (old != 0 || root != 1)) {
+ t.Fatal(err, old, root)
+ }
+ })
+ }
+}
+
+type restartRequestSyncFault struct {
+ *rollbackDisk
+ stage string
+}
+
+func (s *restartRequestSyncFault) Write(name string, raw []byte) error {
+ var r struct {
+ Pending *release.RestartIntent
+ ScopeID string `json:"scope_id"`
+ }
+ if err := json.Unmarshal(raw, &r); err != nil {
+ return err
+ }
+ stage := "baseline"
+ if r.Pending != nil {
+ stage = "pending"
+ if r.Pending.Targets[0].Before != "" {
+ stage = "intent"
+ }
+ if r.Pending.Targets[0].After != "" {
+ stage = "completed"
+ }
+ } else if r.ScopeID != release.RestartScopeNone {
+ stage = "consumed"
+ }
+ if stage != s.stage {
+ return s.rollbackDisk.Write(name, raw)
+ }
+ f, err := os.CreateTemp(s.dir, "unsynced-")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(f.Name())
+ if _, err = f.Write(raw); err != nil {
+ f.Close()
+ return err
+ }
+ if err = f.Sync(); err != nil {
+ f.Close()
+ return err
+ }
+ if err = f.Close(); err != nil {
+ return err
+ }
+ if err = os.Rename(f.Name(), filepath.Join(s.dir, name)); err != nil {
+ return err
+ }
+ return os.ErrPermission
+}
+
+func TestPlan14RootRestartProductionSyncFailure(t *testing.T) {
+ for _, stage := range []string{"baseline", "pending", "intent", "completed", "consumed"} {
+ t.Run(stage, func(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ fault := &restartRequestSyncFault{rollbackDisk: f.p.journal.Store.(*rollbackDisk), stage: stage}
+ f.p.journal.Store = fault
+ if err := f.run(t); err == nil {
+ t.Fatal("failed sync accepted")
+ }
+ want := 0
+ if stage == "completed" || stage == "consumed" {
+ want = 1
+ }
+ if len(f.u.effects) != want {
+ t.Fatal(f.u.effects)
+ }
+ // The read must repair durability before dispatch or completion.
+ _ = f.run(t)
+ if len(f.u.effects) != want {
+ t.Fatal("repeat under failed sync", f.u.effects)
+ }
+ fault.stage = ""
+ err := f.run(t)
+ if stage == "intent" {
+ if err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("ambiguous repeat: %v %v", err, f.u.effects)
+ }
+ f.u.props.InvocationID += "verified"
+ f.u.props.MainPID++
+ if err = f.run(t); err != nil || len(f.u.effects) != 0 {
+ t.Fatalf("repair: %v %v", err, f.u.effects)
+ }
+ } else if err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("repair: %v %v", err, f.u.effects)
+ }
+ })
+ }
+}
+
+func TestPlan14RootRestartProductionCanonicalSelection(t *testing.T) {
+ for _, kind := range []string{"hibernate", "exec", "sidecar"} {
+ t.Run(kind, func(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if kind == "sidecar" {
+ // A valid root sidecar installation cannot hide another app's units.
+ in, err := f.p.source.installation(t.Context(), observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ in.Host = provision.SidecarHost("worker", in.App, in.Account)
+ in.Record = provision.SidecarRecordKey(in.Account, in.App, "worker")
+ claim := *f.claim
+ holder := *claim.Holder
+ claim.Holder = &holder
+ claim.Host = in.Host
+ holder.Key = in.Record
+ g := hostguard.Guard{DataDir: t.TempDir(), LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(f.p.source.state), Claims: rootViewClaimReader{obs: hostclaim.Observed{Host: in.Host, ETag: "sidecar", Claim: &claim}}}
+ if err = g.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ } else {
+ rec, err := provision.GetRecord(t.Context(), f.e.store, "bab", "console", observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if kind == "exec" {
+ rec.Unit = ""
+ } else {
+ yes := true
+ rec.Hibernate = &yes
+ rec.Addr = ""
+ rec.Unit = "carlos-app@" + rec.Host + ".service"
+ }
+ plan14PutRecord(t, f.e.store, *rec)
+ }
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ want := 0
+ if kind == "sidecar" {
+ want = 1
+ }
+ if len(f.u.effects) != want {
+ t.Fatal(kind, f.u.effects)
+ }
+ })
+ }
+}
+
+func TestPlan14RootRestartProductionReplacedInstallation(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ f.claim.Op.Kind = hostclaim.KindLanding
+ _ = f.run(t)
+ in, err := f.p.source.installation(t.Context(), observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.claim.Holder.Stamp++
+ in.Stamp++
+ w := f.p.writes.Selector.(hostbox.WorkSource)
+ if err = w.Guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ f.claim.Op.Kind = hostclaim.KindServing
+ if err = f.run(t); err == nil || len(f.u.effects) != 0 {
+ t.Fatalf("captured request crossed installation: %v %v", err, f.u.effects)
+ }
+}
+
+type restartRequestErrorUnits struct {
+ *plan14RestartUnits
+ after bool
+}
+
+func (u restartRequestErrorUnits) TryRestart(unit string) error {
+ if u.after {
+ if err := u.plan14RestartUnits.TryRestart(unit); err != nil {
+ return err
+ }
+ }
+ return os.ErrPermission
+}
+
+func TestPlan14RootRestartProductionDispatchError(t *testing.T) {
+ for _, after := range []bool{false, true} {
+ t.Run(map[bool]string{false: "no-effect", true: "effect-then-error"}[after], func(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ f.p.sys = restartRequestErrorUnits{f.u, after}
+ if err := f.run(t); err == nil {
+ t.Fatal("lost dispatch error")
+ }
+ b := release.RestartBinding{Box: "box", Account: "bab", App: "console"}
+ pending, err := f.p.journal.Observe(b, release.RestartSnapshot{})
+ if err != nil || pending == nil {
+ t.Fatalf("ambiguous consumed: %+v %v", pending, err)
+ }
+ f.p.sys = f.u
+ err = f.run(t)
+ if after {
+ if err != nil || len(f.u.effects) != 1 {
+ t.Fatal(err, f.u.effects)
+ }
+ } else {
+ if err == nil || len(f.u.effects) != 0 {
+ t.Fatal(err, f.u.effects)
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14RootRestartProductionDefiniteFailure(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ f.u.afterEffect = func() { f.u.props.ActiveState = "failed"; f.u.props.MainPID = 0 }
+ if err := f.run(t); err == nil || len(f.u.effects) != 1 {
+ t.Fatal(err, f.u.effects)
+ }
+ if err := f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("failed request repeated: %v %v", err, f.u.effects)
+ }
+ f.u.afterEffect = nil
+ f.u.props.ActiveState = "active"
+ f.u.props.MainPID = 8
+ f.u.props.InvocationID += "recovered"
+ f.request(t, release.RestartScopeHost, observedHost)
+ if err := f.run(t); err != nil || len(f.u.effects) != 2 {
+ t.Fatalf("new request lost: %v %v", err, f.u.effects)
+ }
+}
+
+type restartRequestShowError struct{ *plan14RestartUnits }
+
+func (u restartRequestShowError) Show(unit string) (unitProps, error) {
+ if len(u.effects) > 0 {
+ return unitProps{}, os.ErrPermission
+ }
+ return u.plan14RestartUnits.Show(unit)
+}
+
+func TestPlan14RootRestartProductionObservationError(t *testing.T) {
+ f := newRestartConsumptionFixture(t)
+ if err := f.run(t); err != nil {
+ t.Fatal(err)
+ }
+ f.request(t, release.RestartScopeHost, observedHost)
+ f.p.sys = restartRequestShowError{f.u}
+ if err := f.run(t); err == nil || len(f.u.effects) != 1 {
+ t.Fatal(err, f.u.effects)
+ }
+ b := release.RestartBinding{Box: "box", Account: "bab", App: "console"}
+ pending, err := f.p.journal.Observe(b, release.RestartSnapshot{})
+ if err != nil || pending == nil {
+ t.Fatalf("observation failure consumed: %+v %v", pending, err)
+ }
+ f.p.sys = f.u
+ if err = f.run(t); err != nil || len(f.u.effects) != 1 {
+ t.Fatalf("observed successor replayed: %v %v", err, f.u.effects)
+ }
+}
diff --git a/internal/host/rootrestart_requests.go b/internal/host/rootrestart_requests.go
new file mode 100644
index 000000000..338bcbb53
--- /dev/null
+++ b/internal/host/rootrestart_requests.go
@@ -0,0 +1,237 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type rootRestartRequests struct {
+ journal *release.RootRestarts
+ source rootRestartRequestSource
+ writes hostwrite.Config
+ sys unitSystem
+ checks rootRestartChecks
+ getenv func(string) string
+ optDir string
+}
+
+// runAdoptUnderRootRelease owns release exclusion for both call sites.
+func productionRestartRequestPass(ctx context.Context, store release.Store, writes hostwrite.Config, oldPass func() error) error {
+ return restartRequestPass(legacyRootRestart, oldPass, func() error {
+ census, err := rootparticipation.Open()
+ if err != nil {
+ return err
+ }
+ defer census.State.Close()
+ state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false)
+ if err != nil {
+ return err
+ }
+ defer state.Close()
+ journal, err := rootstate.Open(release.RootRestartsDir, true, false)
+ if err != nil {
+ return err
+ }
+ defer journal.Close()
+ sys := systemdUnits{}
+ source := productionProofBindingSource(store, sys, release.OptDir())
+ source.cacheIndex = false
+ checks := productionRootRestartChecks(sys, release.OptDir())
+ checks.host = source.forHost
+ p := rootRestartRequests{journal: &release.RootRestarts{Store: journal}, source: rootRestartRequestSource{state: state, store: store, census: census.Census}, writes: writes, sys: sys, checks: checks, getenv: os.Getenv, optDir: release.OptDir()}
+ return p.run(ctx)
+ })
+}
+
+func restartRequestPass(legacy func() (bool, error), oldPass, root func() error) error {
+ old, err := legacy()
+ if err != nil {
+ return err
+ }
+ if !old {
+ return root()
+ }
+ return oldPass()
+}
+
+func (p rootRestartRequests) run(ctx context.Context) error {
+ apps, err := p.source.inventory(ctx)
+ if err != nil {
+ return err
+ }
+ retained, err := p.journal.Bindings()
+ if err != nil {
+ return err
+ }
+ census, err := p.source.census()
+ if err != nil {
+ return err
+ }
+ for _, b := range retained {
+ if b.Box != census.Box {
+ return os.ErrPermission
+ }
+ if _, ok := apps[b]; !ok {
+ apps[b] = nil
+ }
+ }
+ var failures []error
+ for b, targets := range apps {
+ body, id, readErr := p.source.store.Get(ctx, release.RestartGenKey(b.Account, b.App))
+ if errors.Is(readErr, os.ErrNotExist) {
+ body, id, readErr = nil, release.RootRestartAbsent, nil
+ }
+ scopes, _, scopeErr := release.LoadRestartScopes(ctx, p.source.store, b.Account, b.App)
+ in := release.RestartSnapshot{Complete: readErr == nil && scopeErr == nil, AppID: id, AppBody: body, Targets: targets}
+ if scopes != nil {
+ in.Scopes = *scopes
+ }
+ intent, err := p.journal.Observe(b, in)
+ if err != nil {
+ failures = append(failures, errors.Join(err, readErr, scopeErr))
+ continue
+ }
+ if intent == nil {
+ continue
+ }
+ for _, progress := range intent.Targets {
+ if progress.After != "" || progress.Failed {
+ continue
+ }
+ if err := p.consume(ctx, b, intent.ID, progress); err != nil {
+ failures = append(failures, err)
+ }
+ }
+ if err := p.journal.Finish(b, intent.ID); err != nil {
+ failures = append(failures, err)
+ }
+ }
+ return errors.Join(failures...)
+}
+
+func (p rootRestartRequests) consume(ctx context.Context, b release.RestartBinding, id string, progress release.RestartProgress) error {
+ target := progress.Target
+ in, err := p.source.installation(ctx, target.Host)
+ if err != nil {
+ return err
+ }
+ if in.Empty || in.Box != b.Box || in.Account != b.Account || in.App != b.App {
+ return os.ErrPermission
+ }
+ current, err := p.source.target(ctx, in)
+ if err != nil {
+ return err
+ }
+ if current != target {
+ return os.ErrPermission
+ }
+ // A retained census never falls through the flag-off work provider.
+ if !p.writes.Enabled {
+ return os.ErrPermission
+ }
+ work, err := p.writes.Select(ctx, target.Host, "restart")
+ if err != nil {
+ return err
+ }
+ selected, err := hostbox.Selected(work)
+ if err != nil {
+ return err
+ }
+ if selected.Box != b.Box || selected.Captured != (hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}) {
+ return os.ErrPermission
+ }
+ var before proofBinding
+ check := func() error {
+ fresh, err := p.source.target(ctx, in)
+ if err != nil {
+ return err
+ }
+ if fresh != target {
+ return os.ErrPermission
+ }
+ observed, err := p.checks.host(ctx, target.Host)
+ if err != nil {
+ return err
+ }
+ if observed.Route.Account != b.Account || observed.Route.App != b.App || observed.Route.Unit != target.Unit || observed.RecordKey+"@"+observed.RecordETag != target.Source || observed.Invocation == "" || observed.PID <= 0 || !observed.authorizedRecovery() {
+ return os.ErrPermission
+ }
+ props, err := p.sys.Show(target.Unit)
+ if err != nil {
+ return err
+ }
+ if props.ActiveState != "active" || props.MainPID != observed.PID || props.InvocationID != observed.Invocation {
+ return os.ErrPermission
+ }
+ if before.Source != "" && (observed.Source != before.Source || observed.Invocation != before.Invocation || observed.Version != before.Version || observed.Digest != before.Digest) {
+ return os.ErrPermission
+ }
+ before = observed
+ return nil
+ }
+ previous := progress.Before
+ if previous == "" {
+ if err := work.With(ctx, hostwrite.Identity{}, func() error {
+ if err := check(); err != nil {
+ return err
+ }
+ previous = before.Invocation
+ return p.journal.Begin(b, id, target, previous)
+ }); err != nil {
+ return err
+ }
+ }
+ // Re-enter admission after the durable intent. A move that starts during
+ // the write cannot turn a persisted intent into permission to restart.
+ return work.With(ctx, hostwrite.Identity{}, func() error {
+ if err := check(); err != nil {
+ return err
+ }
+ if progress.Before == "" {
+ if err := p.sys.TryRestart(target.Unit); err != nil {
+ return err
+ }
+ }
+ // An ambiguous invocation cannot be dispatched again, even after reboot.
+ opts := settleOpts{Timeout: unitSettleTimeout(p.getenv), Poll: 500 * time.Millisecond, Hold: 5 * time.Second, RequireExe: true, WantDir: filepath.Dir(release.VersionPath(p.optDir, b.Account, before.Version, b.App)), Ready: p.checks.ready(before.Route), Now: p.checks.now, Sleep: p.checks.sleep}
+ if err := settleUnit(p.sys, target.Unit, previous, opts); err != nil {
+ if progress.Before == "" && restartEffectFailed(err) {
+ props, showErr := p.sys.Show(target.Unit)
+ failed, bindingErr := p.checks.host(ctx, target.Host)
+ if showErr == nil && bindingErr == nil && props.ActiveState == "failed" && failed.Source == before.Source && failed.Version == before.Version && failed.Digest == before.Digest {
+ return errors.Join(err, p.journal.Fail(b, id, target))
+ }
+ }
+ return err
+ }
+ after, err := p.checks.host(ctx, target.Host)
+ if err != nil {
+ return err
+ }
+ fresh, err := p.source.target(ctx, in)
+ if err != nil {
+ return err
+ }
+ if fresh != target || after.Source != before.Source || after.Version != before.Version || after.Digest != before.Digest || after.Invocation == previous || after.PID <= 0 {
+ return os.ErrPermission
+ }
+ return p.journal.Complete(b, id, target, after.Invocation)
+ })
+}
+
+func logRestartRequestError(err error) {
+ if err != nil {
+ log.Printf("adopt: root restart requests: %v", err)
+ }
+}
diff --git a/internal/host/rootrestart_requests_source.go b/internal/host/rootrestart_requests_source.go
new file mode 100644
index 000000000..ebe1edd5f
--- /dev/null
+++ b/internal/host/rootrestart_requests_source.go
@@ -0,0 +1,130 @@
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type rootRestartRequestSource struct {
+ state rootstate.Store
+ store release.Store
+ census func() (rootparticipation.Census, error)
+}
+
+func (s rootRestartRequestSource) installation(ctx context.Context, host string) (hostguard.RootInstallation, error) {
+ g := hostguard.Guard{Root: hostguard.NewRootAuthority(s.state)}
+ captured, err := g.Capture(host)
+ if err != nil {
+ return hostguard.RootInstallation{}, err
+ }
+ if captured.Stamp == 0 {
+ return hostguard.RootInstallation{Empty: true}, nil
+ }
+ v, err := g.Root.View(ctx, host, captured)
+ if err != nil {
+ return hostguard.RootInstallation{}, err
+ }
+ c, err := s.census()
+ in := v.Installation
+ if err != nil {
+ return in, err
+ }
+ if !c.Complete || c.NeverEnrolled || in.Empty || in.Box != c.Box || in.Enrollment != c.Enrollment || in.Migration != c.Migration || in.InventoryRevision == 0 {
+ return in, os.ErrPermission
+ }
+ return in, nil
+}
+
+func (s rootRestartRequestSource) target(ctx context.Context, in hostguard.RootInstallation) (release.RestartTarget, error) {
+ if in.Record != provision.RecordKey(in.Account, in.App, in.Host) {
+ return release.RestartTarget{}, os.ErrPermission
+ }
+ rec, revision, err := provision.GetRecordETag(ctx, s.store, in.Account, in.App, in.Host)
+ if err != nil {
+ return release.RestartTarget{}, err
+ }
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil || inc != in.Incarnation {
+ return release.RestartTarget{}, os.ErrPermission
+ }
+ // The agent owns exec children. Only an explicit canonical unit enters
+ // root's unit journal; socket shape cannot invent a unit binding.
+ if rec.Unit == "" {
+ return release.RestartTarget{}, nil
+ }
+ rt, err := rootRecordRoute(rec)
+ if err != nil {
+ return release.RestartTarget{}, err
+ }
+ if !release.Adoptable(rt) || revision == "" {
+ return release.RestartTarget{}, os.ErrPermission
+ }
+ raw, err := json.Marshal(in)
+ if err != nil {
+ return release.RestartTarget{}, err
+ }
+ id := sha256.Sum256(raw)
+ return release.RestartTarget{Host: in.Host, Source: in.Record + "@" + revision, Installation: hex.EncodeToString(id[:]), Unit: rt.Unit, Environment: rec.Environment, Hibernate: rec.HibernateOrDefault()}, nil
+}
+
+func (s rootRestartRequestSource) inventory(ctx context.Context) (map[release.RestartBinding][]release.RestartTarget, error) {
+ if s.state == nil || s.store == nil || s.census == nil {
+ return nil, os.ErrPermission
+ }
+ c, err := s.census()
+ if err != nil {
+ return nil, err
+ }
+ if !c.Complete || c.NeverEnrolled {
+ return nil, os.ErrPermission
+ }
+ names, err := s.state.Names()
+ if err != nil {
+ return nil, err
+ }
+ out := map[release.RestartBinding][]release.RestartTarget{}
+ for _, name := range names {
+ if strings.HasSuffix(name, ".lock") {
+ continue
+ }
+ if !strings.HasSuffix(name, ".json") {
+ return nil, os.ErrPermission
+ }
+ in, err := s.installation(ctx, strings.TrimSuffix(name, ".json"))
+ if err != nil {
+ return nil, err
+ }
+ if in.Empty {
+ continue
+ }
+ // Sidecars use a different record family and have no root restart leg.
+ if in.Record != provision.RecordKey(in.Account, in.App, in.Host) {
+ acct, app, name, ok := provision.ParseSidecarKey(in.Record, map[string]bool{in.Account: true})
+ if ok && acct == in.Account && app == in.App && provision.ValidSidecarName(name) && provision.SidecarHost(name, app, acct) == in.Host {
+ continue
+ }
+ return nil, os.ErrPermission
+ }
+ target, err := s.target(ctx, in)
+ if err != nil {
+ return nil, err
+ }
+ if target.Host == "" {
+ continue
+ }
+ b := release.RestartBinding{Box: c.Box, Account: in.Account, App: in.App}
+ out[b] = append(out[b], target)
+ }
+ return out, nil
+}
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go
index 609de984d..53839474c 100644
--- a/internal/provision/plan14_e_write_census_test.go
+++ b/internal/provision/plan14_e_write_census_test.go
@@ -2,6 +2,9 @@ package provision
// Track E overlays the reviewed Part 1 write census without changing its baseline.
var plan14EWriteCensusOverlay = []censusEntry{
+ {"internal/host/rootrestart_requests.go", "rootRestartRequests.consume", "key-site", "key:RecordKey", "0a9fbbe6f201077e276298b7e666e1ee2a345f1c420335290fec38a676b1ad84"},
+ {"internal/host/rootrestart_requests_source.go", "rootRestartRequestSource.inventory", "key-site", "key:ParseSidecarKey,key:RecordKey", "1ae4bd07fdbd2b0fccd29cc032b0de4a6c53bc52da7fcbc186a11f8b65b394f9"},
+ {"internal/host/rootrestart_requests_source.go", "rootRestartRequestSource.target", "key-site", "key:RecordKey", "301b62efbf0281f713cbed7fb15cd244b7d2c479882f4ad930c8d482b51177ff"},
{"internal/historyreadback/readback.go", "publish", "nonrecord", "sel:Put", "2168932e34cc6f173fde4d4cf0e74a302217ade779c09cc2d1e850ffdab0cb48"},
{"internal/release/roothistory_delivery.go", "PublishHistoryRecoveryRetirement", "nonrecord", "sel:Put", "ec499a70f267b5b551e918c7ebb7ce36745afa87f77160fe9dc826d8dbe3032b"},
{"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "56edaf01411de3813fe327973302b1df35e403d2870625fc341355b5bc5cfa0b"},
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index a93789c81..bd9342e77 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -1,5 +1,5 @@
# Track E integration after merge3. Earlier baselines, overlays and legacy fingerprints are immutable.
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. whole:Route@10480,whole:Route@17841,whole:Route@17877,whole:Route@7901 2cc358516c72149fd1cccf42f1e20765dba30ec8b98e6c83014fb002261b4d3a
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. whole:Route@10700,whole:Route@18080,whole:Route@18116,whole:Route@8002 9fc9698cee7c2b88e3d38203a17e7a41512e800fa172ce5b19272e75c2c83ca5
internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 E1 selects the checked private history, canonical named-unit provider and root mint inventory under release exclusion with claims off or on; reconstruction and restart consumption remain incomplete. file:Open@191 9cdd0a958f5924c9c8d8c5613fd665f2c96a194cd11322adb6f0050fb183b541
internal/host/namedrecovery.go namedUnitRecovery.repair authority E1,J1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@2642,registry:Channel@1814,whole:Route@1060,whole:Route@1822 566bc61eafb118d9f6fba29228c372b18c8729549ed9a0bccf32c61b360eb9c3
internal/host/namedrecovery_active.go namedUnitRecovery.recover authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@5504,registry:Channel@2558,registry:Unit@216,registry:Unit@233,registry:Version@3618,registry:Version@3724,whole:Route@1478,whole:Route@202,whole:Route@2560,whole:Route@3544 dabec20a8ea93c7d72b6d6c3a30a1ae7c99ffcee4de4c0416fba699734739b92
@@ -54,8 +54,8 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S
internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a
internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab
internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7
-@coverage internal/host d2d36da953f0ca69559cbfe67b919e1cf2a66419c67b8c247177b3da8c7cdbdf
-@coverage internal/release 3bc50bded64402b272fe913562fce6f79828338e2356387a9bdc600961fc7153
+@coverage internal/host b2bc360b179563924a103a527049fe2bfd013f3979f43d5585cf281059a0ce39
+@coverage internal/release 41c455979764d29fd7801abfa4700e0d941341a954e37bbe1de00665ae081ec2
@coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05
internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154
internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9
@@ -151,3 +151,14 @@ internal/release/rootrestart_history.go type RestartTarget authority E1 E14 rest
internal/release/rootrestart_history.go type RootRestarts authority E1 E14 restart consumption component. Root-only canonical inputs under release exclusion; production wiring remains pending. Fixtures: TestPlan14RootRestartConsumption, TestPlan14RootRestartSelection, TestPlan14RootRestartReadMustSync, TestPlan14RootRestartProcessCrash. ae897b5a0d3f198ce8fbb4785167e9bb949a48be6b7c386d4b9710f38eee8a69
internal/release/rootrestart_history.go type rootRestartRecord authority E1 E14 restart consumption component. Root-only canonical inputs under release exclusion; production wiring remains pending. Fixtures: TestPlan14RootRestartConsumption, TestPlan14RootRestartSelection, TestPlan14RootRestartReadMustSync, TestPlan14RootRestartProcessCrash. c047024b6ac2b684e65d8728e338896c65712f531cb853596cb74c855c435b1e
internal/release/rootrestart_history.go validRootRestart authority E1 E14 restart consumption component. Root-only canonical inputs under release exclusion; production wiring remains pending. Fixtures: TestPlan14RootRestartConsumption, TestPlan14RootRestartSelection, TestPlan14RootRestartReadMustSync, TestPlan14RootRestartProcessCrash. 2298397712698e769cf455413f949dc7fd4fede1edf84b04ca8c5aaf0bdada86
+internal/host/rootrestart_requests.go logRestartRequestError authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. de113a11aa6f342ee7c1199a96094dda68e25de58b13e4f2aa96ca026bf7b0d5
+internal/host/rootrestart_requests.go productionRestartRequestPass authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. file:Open@218,file:Open@324,file:Open@472 b29fad52fafeb23a5d7fce0d34cde66c79aa1070e9844c78b9fa68135f80b872
+internal/host/rootrestart_requests.go restartRequestPass authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. a8c176e0b5526f49b74a953c58c66bbbac794105d3555725d0c8d90b95ec8731
+internal/host/rootrestart_requests.go rootRestartRequests.consume authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. registry:Account@1196,registry:App@1235,registry:Unit@1266,whole:Route@1205,whole:Route@1244,whole:Route@1275,whole:Route@2938 0a9fbbe6f201077e276298b7e666e1ee2a345f1c420335290fec38a676b1ad84
+internal/host/rootrestart_requests.go rootRestartRequests.run authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. 55416b6e2303523ac666e2a38cc60964bd74dea0488767ba6f735be42e9516f0
+internal/host/rootrestart_requests.go type rootRestartRequests authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. 2ab24e3f7fb2932ed8ac874f78bff6f2d76d9c052044a72ca8501cbd2264717e
+internal/host/rootrestart_requests_source.go rootRestartRequestSource.installation authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. 23eea263231be328beb687006862e773af38d7489e21fcc07ecbfc8107ecff22
+internal/host/rootrestart_requests_source.go rootRestartRequestSource.inventory authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. 1ae4bd07fdbd2b0fccd29cc032b0de4a6c53bc52da7fcbc186a11f8b65b394f9
+internal/host/rootrestart_requests_source.go rootRestartRequestSource.target authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. registry:Unit@1211,whole:Route@891 301b62efbf0281f713cbed7fb15cd244b7d2c479882f4ad930c8d482b51177ff
+internal/host/rootrestart_requests_source.go type rootRestartRequestSource authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. db7ff5c46b6db3fe88657fddf8b4e17324b896fce9b21042d7f0f63140c1d776
+internal/release/rootrestart_history.go RootRestarts.Bindings authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. file:Read@324 d22b7e6b5710ca0025639395101933bec5ce0bd38768ebcf44a0805f1f159b08
diff --git a/internal/release/rootrestart_history.go b/internal/release/rootrestart_history.go
index 8a6c45ca7..4f67230ed 100644
--- a/internal/release/rootrestart_history.go
+++ b/internal/release/rootrestart_history.go
@@ -23,6 +23,36 @@ const RootRestartsDir = "/var/lib/carlos-release/private/restarts"
// marks, registry rows or service RPC payloads establish these inputs.
type RootRestarts struct{ Store rootstate.Store }
+// Bindings retains apps whose captured work outlives their local installation.
+func (h *RootRestarts) Bindings() ([]RestartBinding, error) {
+ if h == nil || h.Store == nil {
+ return nil, os.ErrPermission
+ }
+ names, err := h.Store.Names()
+ if err != nil {
+ return nil, err
+ }
+ var out []RestartBinding
+ for _, name := range names {
+ if strings.HasSuffix(name, ".lock") {
+ continue
+ }
+ raw, err := h.Store.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ var r rootRestartRecord
+ if strictCanonicalJSON(raw, &r) != nil || !validRootRestart(r, r.Binding) || rootRestartKey(r.Binding) != name {
+ return nil, os.ErrPermission
+ }
+ if _, err := h.read(r.Binding); err != nil {
+ return nil, err
+ }
+ out = append(out, r.Binding)
+ }
+ return out, nil
+}
+
type RestartBinding struct {
Box string `json:"box"`
Account string `json:"account"`