WIP claim Plan 14 Part 3 checkpoint 10 (not for merge)
Narrowed scope: E3 sticky veto off in production, E4 refusal-based protection on both convergence paths, J4 limited to enabled families, H2 boundary witnesses; reboot fixtures pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 files changed,
+730
−25
internal/caps/converge.go+11 −0internal/caps/root_authorization_test.go+41 −0internal/caps/rootauthority.go+31 −0internal/host/plan14_root_failures_test.go+38 −4internal/host/rootcaps_root_linux_test.go+154 −0internal/host/rootupdates.go+9 −9internal/host/rootupdates_hardened_root_linux_test.go+129 −0internal/host/rootupdates_legacy_linux_test.go+48 −0internal/host/rootupdates_opener_root_linux_test.go+143 −0internal/host/rootupdates_runtime.go+5 −1internal/host/rootupdates_runtime_test.go+33 −0internal/provision/plan14_input_inventory_test.go+76 −5internal/provision/plan14_p3_census_test.go+1 −1internal/provision/testdata/plan14-root-reads-p3.tsv+11 −5
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 294.649s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.727s
ok github.com/carlosframework/platform/cmd/moveprobe 92.263s
ok github.com/carlosframework/platform/cmd/zddprobe 0.706s
ok github.com/carlosframework/platform/internal/activator 249.925s
ok github.com/carlosframework/platform/internal/agentsock 0.019s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.038s
ok github.com/carlosframework/platform/internal/appconfig 2.455s
ok github.com/carlosframework/platform/internal/apperr 0.088s
ok github.com/carlosframework/platform/internal/applog 0.158s
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.507s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.022s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.702s
ok github.com/carlosframework/platform/internal/changelog 0.322s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 391.998s
? 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.358s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.020s
ok github.com/carlosframework/platform/internal/dbverify 0.013s
ok github.com/carlosframework/platform/internal/dnsrec 0.010s
ok github.com/carlosframework/platform/internal/docsite 0.012s
ok github.com/carlosframework/platform/internal/domains 2.371s
ok github.com/carlosframework/platform/internal/doorkey 0.014s
ok github.com/carlosframework/platform/internal/economics 0.014s
ok github.com/carlosframework/platform/internal/edge 26.779s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.017s
ok github.com/carlosframework/platform/internal/fleetapi 0.145s
ok github.com/carlosframework/platform/internal/gate 1.669s
ok github.com/carlosframework/platform/internal/genselect 0.035s
ok github.com/carlosframework/platform/internal/geo 0.027s
ok github.com/carlosframework/platform/internal/geoip 0.047s
ok github.com/carlosframework/platform/internal/historyreadback 0.160s
ok github.com/carlosframework/platform/internal/host 146.924s
ok github.com/carlosframework/platform/internal/hostactors 0.399s
ok github.com/carlosframework/platform/internal/hostbox 2.524s
ok github.com/carlosframework/platform/internal/hostclaim 0.488s
ok github.com/carlosframework/platform/internal/hostevidence 0.013s
ok github.com/carlosframework/platform/internal/hostexec 4.918s
ok github.com/carlosframework/platform/internal/hostfence 0.044s
ok github.com/carlosframework/platform/internal/hostfiles 10.408s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.027s
ok github.com/carlosframework/platform/internal/hostops 1.572s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.013s
ok github.com/carlosframework/platform/internal/hostquarantine 0.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.455s
ok github.com/carlosframework/platform/internal/hostresolve 0.145s
ok github.com/carlosframework/platform/internal/hostwrite 0.012s
ok github.com/carlosframework/platform/internal/infraview 0.012s
ok github.com/carlosframework/platform/internal/instancectl 4.174s
ok github.com/carlosframework/platform/internal/instanceexec 2.712s
ok github.com/carlosframework/platform/internal/instancefs 0.012s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.020s
ok github.com/carlosframework/platform/internal/layout 1.555s
ok github.com/carlosframework/platform/internal/lease 9.821s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.164s
ok github.com/carlosframework/platform/internal/maintenance 0.082s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.514s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.080s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.120s
ok github.com/carlosframework/platform/internal/moves/archive 0.799s
ok github.com/carlosframework/platform/internal/objstore 0.179s
ok github.com/carlosframework/platform/internal/ops 0.186s
ok github.com/carlosframework/platform/internal/park 0.203s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 34.624s
ok github.com/carlosframework/platform/internal/publicrelease 0.010s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.503s
ok github.com/carlosframework/platform/internal/relay 7.243s
ok github.com/carlosframework/platform/internal/release 21.619s
ok github.com/carlosframework/platform/internal/rollout 3.774s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.058s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.582s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.492s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.594s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.222s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.217s
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.007s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.555s
=== 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: 47513762 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/16 01:08:21 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEDD306AB27719, 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/caps/converge.go b/internal/caps/converge.go| index c0e1baaae..adaedf271 100644 |
| --- a/internal/caps/converge.go |
| +++ b/internal/caps/converge.go |
| @@ -41,6 +41,8 @@ const ( |
| // the converge tests need no systemd, no root, and no /etc — the same seam |
| // shape activator.UnitBackend.Systemctl uses. |
| type Ops struct { |
| + Authorize func() error |
| + |
| EtcDir string // "" = DefaultEtcDir |
| StateRoot string // "" = DefaultStateRoot |
| RunDir string // "" = DefaultRunDir |
| @@ -187,6 +189,9 @@ func DropInPaths(etcDir, host string) (service, socket string) { |
| func Converge(o Ops, p Plan) (Result, error) { |
| var res Result |
| res.Prev.Host = p.Host |
| + if err := o.authorizeMutation(); err != nil { |
| + return res, err |
| + } |
| // The host becomes a filename under /etc/systemd/system, written as |
| // root. Same guard, same reason, same trust level as appconfig.syncHost's |
| @@ -921,6 +926,9 @@ func CheckClaims(plans []Plan) map[string]error { |
| // not ours when the snapshot was taken is not ours to write over or delete |
| // now either. |
| func Restore(o Ops, prev Snapshot) error { |
| + if err := o.authorizeMutation(); err != nil { |
| + return err |
| + } |
| if err := validHost(prev.Host); err != nil { |
| o.logf(" %q: %v", prev.Host, err) |
| return err |
| @@ -951,6 +959,9 @@ func Restore(o Ops, prev Snapshot) error { |
| // not this function's business: rootRestartRoute's try-restart keeps owning them, |
| // byte-identically. |
| func Restart(o Ops, p Plan) error { |
| + if err := o.authorizeMutation(); err != nil { |
| + return err |
| + } |
| if err := validHost(p.Host); err != nil { |
| o.logf(" %q: %v", p.Host, err) |
| return err |
diff --git a/internal/caps/root_authorization_test.go b/internal/caps/root_authorization_test.go| new file mode 100644 |
| index 000000000..0b58d214f |
| --- /dev/null |
| +++ b/internal/caps/root_authorization_test.go |
| @@ -0,0 +1,41 @@ |
| +package caps |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "testing" |
| +) |
| + |
| +func TestPlan14CapabilityMutationAuthorization(t *testing.T) { |
| + for _, operation := range []string{"converge", "restore", "restart"} { |
| + t.Run(operation, func(t *testing.T) { |
| + o, sys, _, users := testOps(t) |
| + p := keymailPlan(t, Grants{Ports: map[int]bool{25: true}}) |
| + if _, err := Converge(o, p); err != nil { |
| + t.Fatal(err) |
| + } |
| + service, socket := DropInPaths(o.EtcDir, p.Host) |
| + beforeService, beforeSocket := string(readFile(t, service)), string(readFile(t, socket)) |
| + sys.reset() |
| + *users = nil |
| + calls := 0 |
| + o.Authorize = func() error { calls++; return os.ErrPermission } |
| + var err error |
| + switch operation { |
| + case "converge": |
| + p.Grants = Grants{} |
| + _, err = Converge(o, p) |
| + case "restore": |
| + err = Restore(o, Snapshot{Host: p.Host}) |
| + case "restart": |
| + err = Restart(o, p) |
| + } |
| + if !errors.Is(err, os.ErrPermission) || calls != 1 || len(sys.calls) != 0 || len(*users) != 0 { |
| + t.Fatal("unauthorized operation", err, calls, sys.calls, *users) |
| + } |
| + if string(readFile(t, service)) != beforeService || string(readFile(t, socket)) != beforeSocket { |
| + t.Fatal("refusal changed files") |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/caps/rootauthority.go b/internal/caps/rootauthority.go| new file mode 100644 |
| index 000000000..e2e5e3389 |
| --- /dev/null |
| +++ b/internal/caps/rootauthority.go |
| @@ -0,0 +1,31 @@ |
| +package caps |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| +) |
| + |
| +func (o Ops) authorizeMutation() error { |
| + if _, err := os.Lstat(filepath.Join(rootparticipation.Namespace, "census.json")); !errors.Is(err, os.ErrNotExist) { |
| + if err != nil { |
| + return err |
| + } |
| + ledger, required, err := rootparticipation.OpenForUpdate() |
| + if ledger.State != nil { |
| + defer ledger.State.Close() |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if required { |
| + return os.ErrPermission |
| + } |
| + } |
| + if o.Authorize != nil { |
| + return o.Authorize() |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/plan14_root_failures_test.go b/internal/host/plan14_root_failures_test.go| index e3ccc5311..515ebb16c 100644 |
| --- a/internal/host/plan14_root_failures_test.go |
| +++ b/internal/host/plan14_root_failures_test.go |
| @@ -120,7 +120,7 @@ func TestPlan14SwapFailureMigrationUnknown(t *testing.T) { |
| } |
| } |
| -func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack string) { |
| +func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack string, activation ...bool) { |
| t.Helper() |
| ctx := context.Background() |
| opt := t.TempDir() |
| @@ -151,7 +151,10 @@ func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack strin |
| census := func() (rootparticipation.Census, error) { |
| return rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Complete: true}, nil |
| } |
| - p := rootUpdateSource{source: rootRestartRequestSource{state: state, store: store, census: census}, state: state, book: book, store: store, pub: pub, binding: &proofBindingSource{}, data: t.TempDir()} |
| + p := rootUpdateSource{enforceFailures: len(activation) == 0, source: rootRestartRequestSource{state: state, store: store, census: census}, state: state, book: book, store: store, pub: pub, binding: &proofBindingSource{}, data: t.TempDir()} |
| + if len(activation) != 0 { |
| + p.publish = func(string, rootRetryStatusRecord) error { return os.ErrPermission } |
| + } |
| rt := provision.RouteFor(rec, p.data, "", false, nil) |
| reg, err := registry.Open(filepath.Join(t.TempDir(), "r.db")) |
| if err != nil { |
| @@ -192,8 +195,21 @@ func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack strin |
| if err = pass(); err != nil { |
| t.Fatal(err) |
| } |
| - if v, _ := release.LiveVersion(opt, rt.Host); v != "" || effects != 0 { |
| - t.Fatal("failed build dispatched", v, effects) |
| + want := "" |
| + if len(activation) != 0 { |
| + want = "v1" |
| + } |
| + if v, _ := release.LiveVersion(opt, rt.Host); v != want || effects != 0 { |
| + t.Fatal("wrong activation failure policy", v, want, effects) |
| + } |
| + if len(activation) != 0 { |
| + if !book.HeldDown(rec.Account, rec.App, "v1", rec.Host) { |
| + t.Fatal("attribution erased") |
| + } |
| + if saved, err := p.resolve(ctx, rec.Host); err != nil || saved.Failed("v1") || saved.Failed("v2") { |
| + t.Fatal("sticky deny active", err) |
| + } |
| + return |
| } |
| raw, _ := state.Read("settings-" + rt.Host + ".json") |
| var saved rootUpdateRecord |
| @@ -296,3 +312,21 @@ func testRootFailureStore(t *testing.T) rootstate.Store { |
| t.Cleanup(func() { s.Close() }) |
| return s |
| } |
| + |
| +func TestPlan14RootFailureActivationGate(t *testing.T) { |
| + for _, attack := range []string{"combined", "served", "channel", "build"} { |
| + t.Run(attack, func(t *testing.T) { |
| + b, err := rollout.InitializeRootFailures(testRootFailureStore(t), "box/en/1", nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = b.Record(rollout.FailureRecord{Account: "bab", App: "km", Build: "v1", Host: "h.example.com", HeldDown: []string{"h.example.com"}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = b.MarkHoldRan("h.example.com", rollout.HoldRan{Account: "bab", App: "km", Build: "v1", Exec: "unsettled"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + testRootSettingsPass(t, b, attack, true) |
| + }) |
| + } |
| +} |
diff --git a/internal/host/rootcaps_root_linux_test.go b/internal/host/rootcaps_root_linux_test.go| new file mode 100644 |
| index 000000000..2f97212a5 |
| --- /dev/null |
| +++ b/internal/host/rootcaps_root_linux_test.go |
| @@ -0,0 +1,154 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "syscall" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/caps" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14RootCapabilitiesBothPaths(t *testing.T) { |
| + if action := os.Getenv("PLAN14_CAPS_HOSTILE"); action != "" { |
| + path := filepath.Join(rootparticipation.Namespace, "census.json") |
| + var err error |
| + if action == "renamed" { |
| + err = os.Rename(path, path+"-old") |
| + } else { |
| + err = os.Remove(path) |
| + } |
| + if !os.IsPermission(err) { |
| + t.Fatal("service UID mutated census", err) |
| + } |
| + return |
| + } |
| + if os.Geteuid() != 0 { |
| + t.Fatal("root required") |
| + } |
| + if _, err := os.Lstat("/var/lib/carlos-claims"); !os.IsNotExist(err) { |
| + t.Fatal("existing authority", err) |
| + } |
| + opt := t.TempDir() |
| + o, effects := passOps(t) |
| + rt := capsUnitRoute("keymail.dev", "keymail") |
| + rt.Caps, rt.Channel = "port:25", "canary/x" |
| + reg := openReg(t, rt) |
| + set := &caps.Set{V: 1, Daemon: true, Ports: []caps.Port{{Port: 25}}} |
| + store := release.NewDirStore(t.TempDir()) |
| + seed, public, _ := release.GenerateKey() |
| + priv, _ := release.ParseSigningKey(seed, "test") |
| + pub, _ := release.ParsePublicKey(public, "test") |
| + for _, v := range []string{"aaaaaaa", "bbbbbbb"} { |
| + m := release.Manifest{Schema: 1, App: rt.App, Version: v, Kind: "binary", Capabilities: set, Artifacts: map[string]release.Artifact{runtimePlatform(): {}}} |
| + if err := release.Ship(t.Context(), store, release.OpsAccount, m, map[string][]byte{runtimePlatform(): []byte(v)}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + promote := func(v string) { |
| + t.Helper() |
| + if err := release.Promote(t.Context(), store, priv, release.OpsAccount, rt.App, rt.Channel, v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + adopt := func() { |
| + t.Helper() |
| + if err := release.Pass(t.Context(), reg, store, pub, opt, nil, release.WithCapabilities(o)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + promote("aaaaaaa") |
| + adopt() |
| + if v, _ := release.LiveVersion(opt, rt.Host); v != "aaaaaaa" { |
| + t.Fatal("no-census adoption refused", v) |
| + } |
| + if err := capabilityPass(reg, opt, o); err != nil { |
| + t.Fatal(err) |
| + } |
| + svc, sock := readDropIns(t, o, rt.Host) |
| + if sock == "" { |
| + t.Fatal("legacy fixture made no drop-in") |
| + } |
| + if err := os.Mkdir("/var/lib/carlos-claims", 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + defer os.RemoveAll("/var/lib/carlos-claims") |
| + state, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + if err = state.Write("census.json", []byte(`{"schema":1}`)); err != nil { |
| + t.Fatal(err) |
| + } |
| + callbacks := 0 |
| + o.Authorize = func() error { callbacks++; return nil } |
| + promote("bbbbbbb") |
| + for _, grant := range []string{"host-role", "doc-engine", "", "port:25,port:587"} { |
| + t.Run(grant, func(t *testing.T) { |
| + current, err := reg.Get(rt.Host) |
| + if err != nil || current == nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.SetCaps(rt.Host, current.Caps, grant); err != nil { |
| + t.Fatal(err) |
| + } |
| + effects.calls, effects.users = nil, nil |
| + if err := capabilityPass(reg, opt, o); err != nil { |
| + t.Fatal(err) |
| + } |
| + adopt() |
| + a, b := readDropIns(t, o, rt.Host) |
| + if a != svc || b != sock || len(effects.calls) != 0 || len(effects.users) != 0 || callbacks != 0 { |
| + t.Fatal("census permitted unit mutation", effects) |
| + } |
| + if v, _ := release.LiveVersion(opt, rt.Host); v != "aaaaaaa" { |
| + t.Fatal("refused adoption repointed", v) |
| + } |
| + }) |
| + } |
| + guarded := o |
| + if err := caps.Restore(guarded, caps.Snapshot{Host: rt.Host}); err == nil { |
| + t.Fatal("restore bypass") |
| + } |
| + if err := caps.Restart(guarded, caps.Plan{Host: rt.Host}); err == nil { |
| + t.Fatal("restart bypass") |
| + } |
| + for _, hostile := range []string{"corrupt", "renamed", "erased"} { |
| + t.Run(hostile, func(t *testing.T) { |
| + path := filepath.Join(rootparticipation.Namespace, "census.json") |
| + if hostile == "corrupt" { |
| + if err := os.WriteFile(path, []byte("corrupt"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else { |
| + binary, err := os.Open(os.Args[0]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer binary.Close() |
| + cmd := exec.Command("/proc/self/fd/3", "-test.run=^TestPlan14RootCapabilitiesBothPaths$") |
| + cmd.ExtraFiles = []*os.File{binary} |
| + cmd.Env = append(os.Environ(), "PLAN14_CAPS_HOSTILE="+hostile) |
| + cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534}} |
| + if out, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatal(err, string(out)) |
| + } |
| + } |
| + if _, err := caps.Converge(guarded, caps.Plan{Host: rt.Host}); err == nil { |
| + t.Fatal("retained census permitted converge") |
| + } |
| + }) |
| + } |
| + a, b := readDropIns(t, o, rt.Host) |
| + if !bytes.Equal([]byte(a+b), []byte(svc+sock)) { |
| + t.Fatal("fixed unit changed") |
| + } |
| +} |
diff --git a/internal/host/rootupdates.go b/internal/host/rootupdates.go| index ddef9c9a1..aa87e2f70 100644 |
| --- a/internal/host/rootupdates.go |
| +++ b/internal/host/rootupdates.go |
| @@ -18,6 +18,7 @@ import ( |
| "github.com/carlosframework/platform/internal/rollout" |
| "github.com/carlosframework/platform/internal/rootparticipation" |
| "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/safefile" |
| ) |
| type rootUpdateRecord struct { |
| @@ -36,6 +37,8 @@ type rootServedBuild struct { |
| } |
| type rootUpdateSource struct { |
| + enforceFailures bool |
| + |
| legacy []byte |
| ready func(registry.Route) error |
| publish func(string, rootRetryStatusRecord) error |
| @@ -54,7 +57,7 @@ func rootUpdatesMigration(c rootparticipation.Census) string { |
| } |
| func rootFailureLegacy(data string) ([]byte, error) { |
| - f, err := os.Open(filepath.Join(data, rollout.FailureFile)) |
| + f, err := safefile.OpenFile(filepath.Join(data, rollout.FailureFile), os.O_RDONLY, 0) |
| if errors.Is(err, os.ErrNotExist) { |
| return nil, nil |
| } |
| @@ -104,13 +107,7 @@ func productionRootUpdates(dd string, probes ...func() (rootparticipation.Ledger |
| if err != nil { |
| return fail(err) |
| } |
| - legacy, err := rootFailureLegacy(dd) |
| - if err != nil { |
| - return fail(err) |
| - } |
| - if err = book.ImportLegacy(legacy); err != nil { |
| - return fail(err) |
| - } |
| + legacy, _ := rootFailureLegacy(dd) |
| fences, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) |
| if err != nil { |
| return fail(err) |
| @@ -239,6 +236,9 @@ func (p rootUpdateSource) resolve(ctx context.Context, host string) (release.Roo |
| } |
| } |
| out = release.RootUpdateInput{Route: rt, Build: saved.Build, Consumed: saved.Consumed, Failed: func(build string) bool { |
| + if !p.enforceFailures { |
| + return false |
| + } |
| served, ok := saved.Served[build] |
| ok = ok && served.Digest != "" && served.Invocation != "" && served.Source != "" && served.Epoch == p.book.RestrictionEpoch(rec.Account, rec.App, build, host) |
| if ok { |
| @@ -257,7 +257,7 @@ func (p rootUpdateSource) resolve(ctx context.Context, host string) (release.Roo |
| if projectedVersion == "" && ptr != nil { |
| projectedVersion = ptr.Version |
| } |
| - if err = p.publishRetry(saved, projectedVersion); err != nil { |
| + if err = p.publishRetry(saved, projectedVersion); err != nil && p.enforceFailures { |
| return release.RootUpdateInput{}, err |
| } |
| if saved.Build.State != nil { |
diff --git a/internal/host/rootupdates_hardened_root_linux_test.go b/internal/host/rootupdates_hardened_root_linux_test.go| new file mode 100644 |
| index 000000000..7e7c2709a |
| --- /dev/null |
| +++ b/internal/host/rootupdates_hardened_root_linux_test.go |
| @@ -0,0 +1,129 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "encoding/json" |
| + "fmt" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/rollout" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14RootRetryHardenedController(t *testing.T) { |
| + if os.Getenv("PLAN14_RETRY_HARDENED") == "1" { |
| + s, err := rootstate.Open(rollout.RootFailuresDir, false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer s.Close() |
| + b, err := rollout.OpenRootFailures(s, "box/en/1") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + p := rootUpdateSource{book: b, publish: publishRootRetry} |
| + if err := p.publishRetry(rootUpdateRecord{Route: registry.Route{Host: "h.example.com", Account: "bab", App: "km"}}, "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + return |
| + } |
| + if os.Geteuid() != 0 { |
| + t.Fatal("root required") |
| + } |
| + for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release", rootRetryProjection, "/run/carlos-control", "/run/carlos"} { |
| + if err := os.Mkdir(path, 0755); err != nil { |
| + t.Fatal("existing fixture", path, err) |
| + } |
| + path := path |
| + t.Cleanup(func() { os.RemoveAll(path) }) |
| + } |
| + state, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| + if err = state.Write("census.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + state.Close() |
| + bookState, err := rootstate.Open(rollout.RootFailuresDir, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + book, err := rollout.InitializeRootFailures(bookState, "box/en/1", nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = book.Record(rollout.FailureRecord{Account: "bab", App: "km", Host: "h.example.com", Build: "v1", HeldDown: []string{"h.example.com"}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + id := book.ID() |
| + bookState.Close() |
| + dir := generationRootRunnerDir(t) |
| + config := filepath.Join(dir, "host.env") |
| + if err := os.WriteFile(config, []byte("CARLOS_HOST_CLAIM=1\nCARLOS_DATA_DIR="+dir+"\nCARLOS_OPT_DIR="+dir+"\n"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + settings, err := readClaimUnitSettings(config) |
| + if err != nil || !settings.rootRetry { |
| + t.Fatal("production settings omitted retry scope", settings, err) |
| + } |
| + binary, err := os.ReadFile(os.Args[0]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + runner := filepath.Join(dir, "runner.test") |
| + if err = os.WriteFile(runner, binary, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err = unitsFS.ReadFile("units/" + claimControlUnit) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var lines []string |
| + for _, line := range strings.Split(string(claimUnitContent(claimControlUnit, raw, settings)), "\n") { |
| + if strings.HasPrefix(line, "Requires=") || strings.HasPrefix(line, "After=") || strings.HasPrefix(line, "ExecStart=") { |
| + continue |
| + } |
| + lines = append(lines, line) |
| + } |
| + const unit = "plan14-p3-retry.service" |
| + path := filepath.Join("/run/systemd/system", unit) |
| + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + content := strings.Join(lines, "\n") + fmt.Sprintf("\nType=oneshot\nEnvironment=PLAN14_RETRY_HARDENED=1\nExecStart=%s -test.run=^TestPlan14RootRetryHardenedController$ -test.v\nTimeoutStartSec=60\nIPAddressDeny=any\n", runner) |
| + if _, err = file.WriteString(content); err != nil { |
| + t.Fatal(err) |
| + } |
| + file.Close() |
| + t.Cleanup(func() { |
| + exec.Command("systemctl", "stop", unit).Run() |
| + os.Remove(path) |
| + exec.Command("systemctl", "daemon-reload").Run() |
| + }) |
| + for _, args := range [][]string{{"daemon-reload"}, {"start", unit}} { |
| + if out, err := exec.Command("systemctl", args...).CombinedOutput(); err != nil { |
| + logs, _ := exec.Command("journalctl", "-u", unit, "--no-pager").CombinedOutput() |
| + t.Fatal(err, string(out), string(logs)) |
| + } |
| + } |
| + s, err := rootstate.Open(rootRetryProjection, false, true) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer s.Close() |
| + raw, err = s.Read("h.example.com.json") |
| + var report rootRetryStatusRecord |
| + if err != nil || json.Unmarshal(raw, &report) != nil || report.HeldBook != id || report.HeldEpoch == 0 { |
| + t.Fatal("hardened controller failed delivery", string(raw), err) |
| + } |
| +} |
diff --git a/internal/host/rootupdates_legacy_linux_test.go b/internal/host/rootupdates_legacy_linux_test.go| new file mode 100644 |
| index 000000000..ea8dc45ca |
| --- /dev/null |
| +++ b/internal/host/rootupdates_legacy_linux_test.go |
| @@ -0,0 +1,48 @@ |
| +package host |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/rollout" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func TestPlan14RootFailureLegacySpecialFiles(t *testing.T) { |
| + for _, kind := range []string{"fifo", "symlink", "regular", "absent"} { |
| + t.Run(kind, func(t *testing.T) { |
| + dir := t.TempDir() |
| + path := filepath.Join(dir, rollout.FailureFile) |
| + switch kind { |
| + case "fifo": |
| + if err := unix.Mkfifo(path, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "symlink": |
| + target := filepath.Join(dir, "target") |
| + if err := os.WriteFile(target, []byte("legacy"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(target, path); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "regular": |
| + if err := os.WriteFile(path, []byte("legacy"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + done := make(chan error, 1) |
| + go func() { _, err := rootFailureLegacy(dir); done <- err }() |
| + select { |
| + case err := <-done: |
| + if (err == nil) != (kind == "regular" || kind == "absent") { |
| + t.Fatal("unsafe legacy reader", kind, err) |
| + } |
| + case <-time.After(time.Second): |
| + t.Fatal("legacy file blocked root") |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/rootupdates_opener_root_linux_test.go b/internal/host/rootupdates_opener_root_linux_test.go| new file mode 100644 |
| index 000000000..24fdb2be7 |
| --- /dev/null |
| +++ b/internal/host/rootupdates_opener_root_linux_test.go |
| @@ -0,0 +1,143 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "encoding/json" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| + |
| + "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/rollout" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14RootFailureProductionOpener(t *testing.T) { |
| + if os.Geteuid() != 0 { |
| + t.Fatal("root required") |
| + } |
| + for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release", rootRetryProjection} { |
| + if err := os.Mkdir(path, 0755); err != nil { |
| + t.Fatal("existing fixture", path, err) |
| + } |
| + path := path |
| + t.Cleanup(func() { os.RemoveAll(path) }) |
| + } |
| + state, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + c := rootparticipation.Census{Schema: 1, Box: "box", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| + raw, _ := json.Marshal(c) |
| + if err = state.Write("census.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + state.Close() |
| + failures, err := rootstate.Open(rollout.RootFailuresDir, true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer failures.Close() |
| + book, err := rollout.InitializeRootFailures(failures, rootUpdatesMigration(c), nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rec := provision.Record{Schema: 1, Host: "h.example.com", Account: "bab", App: "km", Channel: "canary/x", Pin: "v1", CreatedAt: time.Now().UTC()} |
| + if _, err = book.Record(rollout.FailureRecord{Account: rec.Account, App: rec.App, Host: rec.Host, Build: "v1", HeldDown: []string{rec.Host}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + inc, _ := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) |
| + fences, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer fences.Close() |
| + in := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: inc, Stamp: 1, Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, InventoryRevision: 1} |
| + if err = hostguard.NewRootAuthority(fences).RetainMigrationDeny(t.Context(), in, 1); err != nil { |
| + t.Fatal(err) |
| + } |
| + store := release.NewDirStore(t.TempDir()) |
| + plan14PutRecord(t, store, rec) |
| + claim := hostclaim.Claim{V: 2, Host: rec.Host, Gen: 1, Holder: &hostclaim.Holder{Family: "instance", Key: in.Record, Account: rec.Account, App: rec.App, Incarnation: inc, CreatedAt: rec.CreatedAt, Location: "instances/" + rec.Host, Stamp: 1, Session: 1, WriterBox: c.Box}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: c.Box}} |
| + if err = claim.Validate(); err != nil { |
| + t.Fatal(err) |
| + } |
| + key, _ := hostclaim.Key(rec.Host) |
| + raw, _ = json.Marshal(claim) |
| + if _, err = store.Put(t.Context(), key, raw, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + seed, public, _ := release.GenerateKey() |
| + priv, _ := release.ParseSigningKey(seed, "test") |
| + pub, _ := release.ParsePublicKey(public, "test") |
| + m := release.Manifest{Schema: 1, App: rec.App, Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{runtimePlatform(): {}}} |
| + if err = release.Ship(t.Context(), store, rec.Account, m, map[string][]byte{runtimePlatform(): []byte("v1")}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = release.Promote(t.Context(), store, priv, rec.Account, rec.App, rec.Channel, "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + data, opt := t.TempDir(), t.TempDir() |
| + if err = os.Mkdir(filepath.Join(data, rollout.FailureFile), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + rt := provision.RouteFor(rec, data, "", false, nil) |
| + rt.Account, rt.App, rt.Channel, rt.GoodVersion = "bad", "foreign", "forged", "forged" |
| + reg := openReg(t, rt) |
| + if err = release.Pass(t.Context(), reg, store, pub, opt, nil, updatesPassOption(data, true)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if v, _ := release.LiveVersion(opt, rec.Host); v != "v1" { |
| + t.Fatal("sticky or legacy state held production adoption", v) |
| + } |
| + opened, err := productionRootUpdates(data)(t.Context(), store, pub) |
| + if err != nil || opened == nil { |
| + t.Fatal(err) |
| + } |
| + resolved, err := opened.Resolve(t.Context(), rec.Host) |
| + if err != nil || resolved.Route.Account != rec.Account || resolved.Route.App != rec.App || resolved.Failed("v1") { |
| + t.Fatal("canonical activation policy", resolved, err) |
| + } |
| + opened.Close() |
| + wire, err := release.SignHostRetry(release.RootSigner(priv), rec.Account, rec.App, rec.Host, "v1", book.ID(), book.PublishedEpoch(rec.Account, rec.App, "v1", rec.Host), time.Now()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + retries := release.ScopedRestartGeneration{Schema: 1, Requests: []release.RestartRequest{{ID: "retry", RequestedAt: time.Now().UTC().Format(time.RFC3339), Scope: release.RestartScope{Kind: release.RestartScopeRetry, Value: rec.Host, Signed: wire}}}} |
| + raw, _ = json.Marshal(retries) |
| + if _, err = store.Put(t.Context(), release.HostRetryKey(rec.Account, rec.App), raw, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + opened, err = productionRootUpdates(data)(t.Context(), store, pub) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = opened.Resolve(t.Context(), rec.Host); err != nil { |
| + t.Fatal(err) |
| + } |
| + opened.Close() |
| + book, err = rollout.OpenRootFailures(failures, rootUpdatesMigration(c)) |
| + if err != nil || book.HeldDown(rec.Account, rec.App, "v1", rec.Host) { |
| + t.Fatal("production retry not settled", err) |
| + } |
| + _, etag, _ := store.Get(t.Context(), key) |
| + claim.Op.Exec = "foreign" |
| + raw, _ = json.Marshal(claim) |
| + if _, err = store.Put(t.Context(), key, raw, etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + opened, err = productionRootUpdates(data)(t.Context(), store, pub) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer opened.Close() |
| + if _, err = opened.Resolve(t.Context(), rec.Host); err == nil { |
| + t.Fatal("foreign controller authority accepted") |
| + } |
| +} |
diff --git a/internal/host/rootupdates_runtime.go b/internal/host/rootupdates_runtime.go| index 8afd0758c..b335f2509 100644 |
| --- a/internal/host/rootupdates_runtime.go |
| +++ b/internal/host/rootupdates_runtime.go |
| @@ -86,7 +86,11 @@ func (p rootUpdateSource) beginRuntime(saved *rootUpdateRecord, save func() erro |
| if err := p.book.End(rt.Host, attempt); err != nil { |
| return err |
| } |
| - return errors.Join(readinessErr, p.publishRetry(*saved, version)) |
| + deliveryErr := p.publishRetry(*saved, version) |
| + if !p.enforceFailures { |
| + deliveryErr = nil |
| + } |
| + return errors.Join(readinessErr, deliveryErr) |
| }, nil |
| } |
diff --git a/internal/host/rootupdates_runtime_test.go b/internal/host/rootupdates_runtime_test.go| index eaed57b0f..5f0d17679 100644 |
| --- a/internal/host/rootupdates_runtime_test.go |
| +++ b/internal/host/rootupdates_runtime_test.go |
| @@ -107,3 +107,36 @@ func TestPlan14RootRetryProjection(t *testing.T) { |
| t.Fatal("stale projection cleared new failure") |
| } |
| } |
| + |
| +func TestPlan14RootFailureDeliveryCannotHoldActivation(t *testing.T) { |
| + for _, ready := range []bool{false, true} { |
| + t.Run(map[bool]string{false: "unready", true: "ready"}[ready], func(t *testing.T) { |
| + book, err := rollout.InitializeRootFailures(testRootFailureStore(t), "box/en/1", nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + saved := rootUpdateRecord{Route: registry.Route{Host: "h.example.com", Account: "bab", App: "km"}, Installation: "install"} |
| + delivered, verified := 0, 0 |
| + p := rootUpdateSource{book: book, publish: func(string, rootRetryStatusRecord) error { delivered++; return errors.New("delivery unavailable") }, ready: func(registry.Route) error { |
| + if !ready { |
| + return errors.New("unready") |
| + } |
| + return nil |
| + }} |
| + finish, err := p.beginRuntime(&saved, func() error { return nil }, "v1", "digest", "attempt") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + err = finish("invocation", nil, func() error { verified++; return nil }) |
| + if (err == nil) != ready || delivered != 1 { |
| + t.Fatal("delivery became launch authority or readiness was waived", err, delivered) |
| + } |
| + if ready && (verified != 1 || saved.Served["v1"].Invocation != "invocation") { |
| + t.Fatal("settlement lost") |
| + } |
| + if !ready && !book.HeldDown("bab", "km", "v1", "h.example.com") { |
| + t.Fatal("failure attribution lost") |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/provision/plan14_input_inventory_test.go b/internal/provision/plan14_input_inventory_test.go| index a0f1ebb42..360caca2f 100644 |
| --- a/internal/provision/plan14_input_inventory_test.go |
| +++ b/internal/provision/plan14_input_inventory_test.go |
| @@ -35,7 +35,6 @@ var inputFamilies = map[string]inputFamily{ |
| "E1": {"E", "TestPlan14RootHistoryProductionPass", "TestPlan14RootHistoryLegacyTampering", "TestPlan14RootHistoryLegacyTampering", "TestPlan14HistoryRetirementProcessCrash", "TestPlan14RootHistoryProductionPass"}, |
| "E2": {"E", "TestPlan14GenerationPrivateJournalRootRunner", "TestPlan14GenerationPayloadPhysicalRootRunner", "TestPlan14GenerationDeletionCannotUnpin", "TestPlan14GenerationCompletenessDurability", "TestPlan14GenerationInstalledUnitRootRunner"}, |
| "E3": {"E", "TestPlan14SwapFailureHostileRootRunner", "TestPlan14SwapFailureSurvivesLegacyTampering", "TestPlan14SwapFailureSurvivesLegacyTampering", "TestPlan14SwapFailureHostileRootRunner", "TestPlan14SwapFailureVerifiedRetry"}, |
| - "E4": {"E", "", "", "", "", ""}, |
| "F": {"F", "TestPlan14RootFloorCrashOrderingRootRunner", "TestPlan14FloorRiseWithoutPlatformUpdateRootRunner", "TestPlan14RootFloorPublicDenySurvivesPrivateLoss", "TestPlan14RootFloorSyscallCrashOrdering", "TestPlan14RootFloorBootSelectors"}, |
| "G": {"G", "TestPlan14RootMigrationHistoricEvidence", "TestPlan14RootMigrationHistoricRootRunner", "TestPlan14RootMigrationHistoricRootRunner", "TestPlan14RootMigrationCrashResume", "TestPlan14RootMigrationBootstrapRootRunner"}, |
| } |
| @@ -44,7 +43,7 @@ func inputFamilyFor(e classifiedRootRead) ([]string, error) { |
| var names []string |
| seen := map[string]bool{} |
| for _, owner := range strings.Split(e.Task, ",") { |
| - if owner == "J1" || owner == "J2" || owner == "J3" || owner == "J4" { |
| + if owner == "J1" || owner == "J2" || owner == "J3" || owner == "J4" || owner == "E4" { |
| continue |
| } |
| family, ok := inputFamilies[owner] |
| @@ -378,8 +377,28 @@ func checkInputExemptions(inv rootReadInventory, callers map[string]string, exem |
| return failures |
| } |
| +func inputActivationEnabled(e classifiedRootRead) bool { |
| + if inputExemptions[e.File+":"+e.Unit].Ruling != "" { |
| + return true |
| + } |
| + for _, prefix := range []string{ |
| + "internal/hostrelocate/", "internal/hostclaim/relocat", |
| + "internal/host/relocation", "internal/activator/worker", |
| + } { |
| + if strings.HasPrefix(e.File, prefix) { |
| + return false |
| + } |
| + } |
| + for _, owner := range strings.Split(e.Task, ",") { |
| + if owner != "E4" && owner != "J1" && owner != "J2" && owner != "J3" && owner != "J4" { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| func checkInputRow(e classifiedRootRead, callers map[string]string, tests map[string]bool) []string { |
| - if e.Class != "authority" { |
| + if e.Class != "authority" || !inputActivationEnabled(e) { |
| return nil |
| } |
| key := e.File + ":" + e.Unit |
| @@ -414,16 +433,36 @@ func TestPlan14RootInputInventoryCoverage(t *testing.T) { |
| t.Fatal(err) |
| } |
| failures := checkInputExemptions(inv, callers, inputExemptions, inputTrackEComplete) |
| + var report []string |
| for key, e := range inv.Entries { |
| if inv.Removed[key] != "" { |
| continue |
| } |
| failures = append(failures, checkInputRow(e, callers, tests)...) |
| if e.Class == "authority" { |
| - family, _ := inputFamilyFor(e) |
| - t.Logf("J4\t%s\t%s\t%s\t%s\t%s", e.File, e.Unit, strings.Join(family, "+"), callers[e.File+":"+e.Unit], e.Basis) |
| + family, familyErr := inputFamilyFor(e) |
| + if familyErr != nil { |
| + family = []string{e.Task} |
| + } |
| + mode := "enabled" |
| + if !inputActivationEnabled(e) { |
| + mode = "report-only" |
| + } |
| + report = append(report, fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s", mode, e.File, e.Unit, strings.Join(family, "+"), callers[e.File+":"+e.Unit], e.Basis)) |
| } |
| } |
| + sort.Strings(report) |
| + for _, row := range report { |
| + t.Log("J4\t" + row) |
| + } |
| + path := os.Getenv("PLAN14_J4_REPORT") |
| + if path == "" { |
| + path = filepath.Join(t.TempDir(), "plan14-root-input-report.tsv") |
| + } |
| + if err := os.WriteFile(path, []byte("scope\tfile\tunit\tfamily\tcaller\tbasis\n"+strings.Join(report, "\n")+"\n"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Logf("J4 report: %s", path) |
| sort.Strings(failures) |
| last := "" |
| for _, failure := range failures { |
| @@ -547,3 +586,35 @@ func TestPlan14RootInputExemptionGraphMutation(t *testing.T) { |
| t.Fatal("production relocation wiring escaped the exemption") |
| } |
| } |
| + |
| +func TestPlan14ActivationInventoryScope(t *testing.T) { |
| + e := classifiedRootRead{rootReadEntry: rootReadEntry{File: "root.go", Unit: "effect"}, Class: "authority", Task: "E4,J4"} |
| + if inputActivationEnabled(e) || len(checkInputRow(e, nil, nil)) != 0 { |
| + t.Fatal("dropped E4 slots enforced") |
| + } |
| + for _, owner := range []string{"A", "B", "B2", "B3", "B4", "C", "D", "E1", "E2", "E3", "F", "G"} { |
| + e.Task = owner + ",E4,J4" |
| + if !inputActivationEnabled(e) || len(checkInputRow(e, nil, nil)) == 0 { |
| + t.Fatal("enabled mixed family waived", owner) |
| + } |
| + f := inputFamilies[owner] |
| + for _, absent := range []string{f.Hostile, f.Rename, f.Erasure, f.Retry, f.Positive} { |
| + tests := map[string]bool{} |
| + for _, name := range []string{f.Hostile, f.Rename, f.Erasure, f.Retry, f.Positive} { |
| + tests[name] = true |
| + } |
| + delete(tests, absent) |
| + if len(checkInputRow(e, map[string]string{"root.go:effect": "main.go:main"}, tests)) == 0 { |
| + t.Fatal("missing enabled fixture accepted", owner, absent) |
| + } |
| + } |
| + } |
| + e.File, e.Unit, e.Task = "internal/hostrelocate/worker.go", "Worker.disabled", "unknown" |
| + if inputActivationEnabled(e) || len(checkInputRow(e, nil, nil)) != 0 { |
| + t.Fatal("frozen family enforced") |
| + } |
| + e.Unit, e.Task = "Worker.Start", "C,D" |
| + if !inputActivationEnabled(e) || len(checkInputRow(e, nil, nil)) == 0 { |
| + t.Fatal("exemption escaped scope") |
| + } |
| +} |
diff --git a/internal/provision/plan14_p3_census_test.go b/internal/provision/plan14_p3_census_test.go| index 55cd3bc40..273fba55e 100644 |
| --- a/internal/provision/plan14_p3_census_test.go |
| +++ b/internal/provision/plan14_p3_census_test.go |
| @@ -43,7 +43,7 @@ func plan14P3WriteCensusInventory() []censusEntry { |
| censusEntry{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "97f1501b9543e36c6a231170425c5c79c7c22489500aadf119e064abc1abde93"}, |
| censusEntry{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "b83ba979dde73b56fbf78b6200edbc5374a3df5bea8150ad26093e64611b69bd"}, |
| censusEntry{"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "578d6a9c37683852391c7ddfc612bb7f7d750b9e4316707daac98dfc5dc2fb88"}, |
| - censusEntry{"internal/host/rootupdates.go", "rootUpdateSource.resolve", "key-site", "key:RecordKey", "ef717e41d930053e3eff1301d87335666589d38e420cf8075b91226123bd3ba7"}, |
| + censusEntry{"internal/host/rootupdates.go", "rootUpdateSource.resolve", "key-site", "key:RecordKey", "e2247ec55dae1342cccc71cb84f8dd8dc901968ba8453bbc2af572c78c8505d3"}, |
| censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.refresh", "key-site", "key:writeRecord", "cb70a1eefbb4f8f243090270f23141c9c9a773ee278af002c79a6c3ef6b7c3b7"}, |
| censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.persist", "key-site", "key:writeRecord", "2c1e0843c5865a31c0fab011d7d4825963bbb5a301727f37f3b1b6c3519eba5e"}, |
| censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.writeRecord", "key-site", "key:writeRecord", "a8a51bdd73f8ab1447edf2241d19a41ba1bcb0084ef045d370e5282aca7e9e66"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv| index a9b44adc2..f4b13f1a8 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-p3.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-p3.tsv |
| @@ -201,7 +201,7 @@ internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory aut |
| @coverage internal/activator 95ea5c811cee8a679170f708c30b4446021740977474551c1d22bf8afaacf9be |
| @coverage internal/console 6f4998c39e6d66cd73ea98500000cbecac251dd21dafd58f51a85c2291e0c533 |
| @coverage internal/edgesfile 7b48368150a427e4620f1c9ded49942b688c9e6c9cd983b7da43d3f2c0810b72 |
| -@coverage internal/host 280444e2c3174a0d9bb07499f47d79a590e5736cb54f7cc7aa7ba679d4af5855 |
| +@coverage internal/host 4d660a8d30b523f1ee4d504d45b48ac8ab791fe8e7483f43da57af6502198bbf |
| @coverage internal/host/units bf6ae516012701e0ca41908167e4efd60dd3c2c24e9af1555dec077e8be44461 |
| @coverage internal/hostbox 1aeb3bed428b63552f65f92bd7fa40bfbd818e34ebd1abfa169f1ea79a7a5d81 |
| @coverage internal/hostfloor 696e5ea4fa37b924b68e8e262235b99298ee19342d875213ba8209e5287dd3c6 |
| @@ -217,9 +217,9 @@ internal/host/rootgenerations_launch.go productionRootGenerations authority E2,E |
| internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete.;rev3=PROPOSED-p3;run=8;identity=root claim launch uid 0;basis=captures persistent executable and tree proofs only after canonical installation and signed artifact verification under exclusion file:Open@2501 578d6a9c37683852391c7ddfc612bb7f7d750b9e4316707daac98dfc5dc2fb88 |
| internal/host/rootgenerations_launch.go type rootGenerationSource authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=binds production runtime authority and persistent file capture to the root launch source ebe058661c48feb225bfb03a6d0b93f116639f06e50a81c51fded1da744043e4 |
| internal/host/rootupdates.go initializeMigrationFailures authority G,E3 rev3=PROPOSED-p3;run=8;identity=offline journal-migrate uid 0;basis=requires complete root census before creating migration-bound root failure book; missing legacy history supplies no known refusal file:Open@254 5f7f652988f3def733e1b525260a52fac3568242960468e5457975ced1cd41d8 |
| -internal/host/rootupdates.go productionRootUpdates authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=opens only census-bound root failure state and imports negative legacy evidence; binds root readiness and retry projection producers file:Open@1243,file:Open@765,file:Read@1566,whole:Route@1879,whole:Route@2200 d56da3910afe6909f05c057267caa6b34217ed26d1d1a445d6d9ce72a429d53c |
| -internal/host/rootupdates.go rootFailureLegacy authority E3 rev3=PROPOSED-p3;run=8;identity=carlos adopt and offline migration uid 0;basis=bounded legacy byte read feeds only failure tuples and unresolved intents to the root deny importer; no legacy book opener or retry grant file:Open@65,file:ReadAll@239 313d92c63ac120966bbe6ad50de446c7a92e2d69d5e18b1b300fd724b3098f78 |
| -internal/host/rootupdates.go rootUpdateSource.resolve authority E3,J1,J3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=resolves canonical settings and signed retries; saves exact old legacy-book acknowledgements only after durable independent root retry file:Read@1368,registry:Account@1543,registry:Account@2428,registry:App@1581,registry:App@2473,registry:BuildState@1210,registry:GoodDemand@1176,registry:GoodDigest@1161,registry:GoodVersion@1145,registry:Host@1515,registry:Host@2393,registry:ServingVersion@1191,registry:Unit@2124,whole:Route@1521,whole:Route@1549,whole:Route@1587,whole:Route@1756,whole:Route@1763,whole:Route@2183,whole:Route@2406,whole:Route@2441,whole:Route@2486,whole:Route@3254,whole:Route@3262,whole:Route@4890,whole:Route@4897 ef717e41d930053e3eff1301d87335666589d38e420cf8075b91226123bd3ba7 |
| +internal/host/rootupdates.go productionRootUpdates authority E3 rev3=PROPOSED-p3;run=10;identity=root claim controller and adopt uid 0;basis=keeps canonical root inputs and retry; leaves sticky failure enforcement disabled; legacy bytes only identify an optional exact retry mirror and cannot block opening or inject fresh denials file:Open@1124,file:Open@765,file:Read@1447,whole:Route@1760,whole:Route@2081 ce81dce48e37f4055efeb3fd8d3562057e504ec5cd74ccbef42c0b8fcd6f20f0 |
| +internal/host/rootupdates.go rootFailureLegacy authority E3 rev3=PROPOSED-p3;run=10;identity=root adoption and offline migration uid 0;basis=bounded nonblocking regular-file read supplies optional exact retry mirror identity or one-time migration deny evidence; refuses symlinks and special files file:OpenFile@65,file:ReadAll@265 efae62d1b996f14baa592e39e0cc22e0f5c8cde5cd94f429498d41039f5a70e4 |
| +internal/host/rootupdates.go rootUpdateSource.resolve authority E3,J1,J3 rev3=PROPOSED-p3;run=10;identity=root claim controller and adopt uid 0;basis=binds canonical settings and retries; gates sticky root failure veto and projection delivery errors off while failure lifecycle acceptance is incomplete file:Read@1368,registry:Account@1543,registry:Account@2428,registry:App@1581,registry:App@2473,registry:BuildState@1210,registry:GoodDemand@1176,registry:GoodDigest@1161,registry:GoodVersion@1145,registry:Host@1515,registry:Host@2393,registry:ServingVersion@1191,registry:Unit@2124,whole:Route@1521,whole:Route@1549,whole:Route@1587,whole:Route@1756,whole:Route@1763,whole:Route@2183,whole:Route@2406,whole:Route@2441,whole:Route@2486,whole:Route@3254,whole:Route@3262,whole:Route@4890,whole:Route@4897 e2247ec55dae1342cccc71cb84f8dd8dc901968ba8453bbc2af572c78c8505d3 |
| internal/host/updatesroot.go updatesPassOption authority E3 rev3=PROPOSED-p3;run=8;identity=root adopt uid 0; unchanged agent opener uid carlos;basis=installs census-only root input provider while preserving the legacy failure-book opener for nonparticipating passes 4f66774059952bda531435d68487fa032a95b8d1c8ca33ec8b71b6639fa117e3 |
| internal/release/adopt.go PassFilter authority B3,E3,J1,E1,E2 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. E7 applies merge ruling 4: unbound no-census routes use the legacy pass and restart; ordinary candidates make no binding store call. Fixtures: TestR2UnguardedERecoveryUpdate, TestR2UnguardedERecoveryIncidentRecipe, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix. E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=resolves protected route inputs before filters, recovery and dispatch on census boxes and bypasses service-owned failure opener only there registry:App@26795,registry:App@29252,registry:App@29744,registry:App@29981,registry:App@31388,registry:App@6590,registry:App@9615,registry:App@9756,registry:Hibernate@29627,registry:Host@1061,registry:Host@1426,registry:Host@29797,registry:Host@30713,registry:Host@31441,registry:Host@3263,registry:Host@3330,registry:Host@3426,registry:Host@3461,registry:UpdateSettings@9493,whole:Route@1046,whole:Route@1164,whole:Route@1188,whole:Route@1461,whole:Route@1622,whole:Route@26790,whole:Route@2726,whole:Route@2768,whole:Route@2877,whole:Route@2901,whole:Route@29247,whole:Route@2932,whole:Route@29505,whole:Route@29527,whole:Route@29574,whole:Route@29592,whole:Route@29602,whole:Route@29645,whole:Route@29666,whole:Route@29679,whole:Route@29739,whole:Route@29765,whole:Route@29839,whole:Route@29878,whole:Route@2989,whole:Route@29894,whole:Route@29922,whole:Route@29976,whole:Route@30002,whole:Route@3075,whole:Route@30755,whole:Route@30809,whole:Route@30825,whole:Route@3097,whole:Route@31332,whole:Route@31383,whole:Route@31409,whole:Route@31483,whole:Route@31518,whole:Route@31534,whole:Route@3197,whole:Route@4280,whole:Route@4927,whole:Route@6512,whole:Route@6534,whole:Route@6585,whole:Route@6611,whole:Route@761,whole:Route@803,whole:Route@9444,whole:Route@950,whole:Route@9558,whole:Route@9603,whole:Route@9751,whole:Route@988 270763804170539d06afd58252d86e1a8ff692ab4d9355be3a2127e9efc9d4a2 |
| internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=applies root settings to canonically resolved routes including named units while legacy settings eligibility remains unchanged registry:App@10936,registry:App@11096,registry:App@12125,registry:App@13051,registry:App@14633,registry:App@19792,registry:App@25268,registry:App@3144,registry:App@33027,registry:App@6758,registry:App@8059,registry:App@8253,registry:Hibernate@33888,registry:Host@10816,registry:Host@10849,registry:Host@10949,registry:Host@11109,registry:Host@11211,registry:Host@11243,registry:Host@11308,registry:Host@11758,registry:Host@11891,registry:Host@12338,registry:Host@12447,registry:Host@12649,registry:Host@13141,registry:Host@13927,registry:Host@14405,registry:Host@14619,registry:Host@14961,registry:Host@15551,registry:Host@19282,registry:Host@19390,registry:Host@19760,registry:Host@20580,registry:Host@20738,registry:Host@2105,registry:Host@21200,registry:Host@21252,registry:Host@21399,registry:Host@21486,registry:Host@22592,registry:Host@22836,registry:Host@2294,registry:Host@23496,registry:Host@2375,registry:Host@23881,registry:Host@24022,registry:Host@25236,registry:Host@25363,registry:Host@26705,registry:Host@27355,registry:Host@30588,registry:Host@31206,registry:Host@31531,registry:Host@3157,registry:Host@31605,registry:Host@31852,registry:Host@32003,registry:Host@33199,registry:Host@33366,registry:Host@33473,registry:Host@33701,registry:Host@33844,registry:Host@34589,registry:Host@34715,registry:Host@34803,registry:Host@4469,registry:Host@4583,registry:Host@5546,registry:Host@5651,registry:Host@6870,registry:Host@6918,registry:Host@7035,registry:Host@7067,registry:Host@7945,registry:Host@8113,registry:Host@8239,registry:Host@8358,registry:Host@8836,registry:Host@9223,registry:Host@9449,registry:Host@9545,registry:Host@9636,registry:Kind@554,registry:Unit@31751,registry:Unit@579,whole:Route@10073,whole:Route@10931,whole:Route@11091,whole:Route@12115,whole:Route@13041,whole:Route@14601,whole:Route@15166,whole:Route@15527,whole:Route@16750,whole:Route@17053,whole:Route@17209,whole:Route@19539,whole:Route@19683,whole:Route@19782,whole:Route@2206,whole:Route@22321,whole:Route@22720,whole:Route@25258,whole:Route@30159,whole:Route@3139,whole:Route@33175,whole:Route@3595,whole:Route@4347,whole:Route@6753 a9672d71a3d70ab0bcc2b257daebb974da0f0bf67731303f51221b216f06eff6 |
| @@ -268,7 +268,7 @@ internal/host/rootupdates_runtime.go beginRootGenerationUpdate authority E2,E3 r |
| internal/host/rootupdates_runtime.go publishRootRetry authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=writes root-owned public retry status only from the independently verified root book file:Open@84 e3c680d5a7d3c423dc183319bb3d0518bbea6cdc752c7bd6b540d57f884b414e |
| internal/host/rootupdates_runtime.go rootRetryStatus hint E3 rev3=PROPOSED-p3;identity=carlos agent status probe uid carlos;basis=agent reads root-owned public retry status; an exact legacy-book acknowledgement may clear only the corresponding local mirror, never root authority file:Lstat@89,file:Open@183,file:Read@721 7db6b16062bd095634a03c58cfdcd3dde2d49c990a34fe37d260d0908a16aa5f |
| internal/host/rootupdates_runtime.go rootUpdateReady authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=probes the canonical deployment endpoint before root records runtime served/request success whole:Route@95 fe93218ee43c1346b148e4c9f6f55731d8865acde4f72ec85f60c5732a6a7d76 |
| -internal/host/rootupdates_runtime.go rootUpdateSource.beginRuntime authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=persists a root intent before effects; independently records failures or readiness plus exact process revalidation and request completion file:Exec@1480,registry:Account@1228,registry:Account@1439,registry:Account@1882,registry:Account@533,registry:App@1245,registry:App@1456,registry:App@1894,registry:App@550,registry:Host@1069,registry:Host@1259,registry:Host@1328,registry:Host@1405,registry:Host@1594,registry:Host@1911,registry:Host@2334,registry:Host@500,whole:Route@180,whole:Route@891 aca5ab8b339bfd54edb2a27d0dc058cc696ff74e669bb17376d426eea0e63eb4 |
| +internal/host/rootupdates_runtime.go rootUpdateSource.beginRuntime authority E3 rev3=PROPOSED-p3;run=10;identity=root claim controller uid 0;basis=retains durable attribution readiness and exact process settlement; diagnostic retry delivery cannot hold a successful launch while sticky enforcement is disabled file:Exec@1480,registry:Account@1228,registry:Account@1439,registry:Account@1882,registry:Account@533,registry:App@1245,registry:App@1456,registry:App@1894,registry:App@550,registry:Host@1069,registry:Host@1259,registry:Host@1328,registry:Host@1405,registry:Host@1594,registry:Host@1911,registry:Host@2334,registry:Host@500,whole:Route@180,whole:Route@891 ea1cb8473da2d46ca21be5c3cc5c1a8108d88b7934427574ea4af55d5d0bfc67 |
| internal/host/rootupdates_runtime.go rootUpdateSource.publishRetry authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=projects the root book identity and epoch and includes a mirror acknowledgement only after the root denial is durably cleared registry:Account@284,registry:Account@378,registry:App@305,registry:App@399,registry:Host@333,registry:Host@427,registry:Host@490,whole:Route@290,whole:Route@311,whole:Route@339,whole:Route@384,whole:Route@405,whole:Route@433,whole:Route@496 d18d5c78c905754df748c0bf887a7170f6378a6da76df196af7508f28372be28 |
| internal/host/rootupdates_runtime.go rootUpdateSource.retryStatus authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=derives retry identity and restrictions from root-held failures and epoch-bound served observations registry:Account@154,registry:Account@380,registry:Account@518,registry:Account@586,registry:Account@644,registry:Account@795,registry:Account@888,registry:App@171,registry:App@403,registry:App@530,registry:App@598,registry:App@656,registry:App@807,registry:App@900,registry:Host@351,registry:Host@544,registry:Host@612,registry:Host@670,registry:Host@821,registry:Host@914,whole:Route@112 a3eafae0a95559cfb0c7677215d89dbf353e44150453f252fed0e56b575e1f11 |
| internal/hostbox/coldhandover.go ColdHandover.Run authority C,J2,J4 rev3=PROPOSED-p3;identity=root claim retirement or offline migration uid 0; shared helpers also run for legacy agent requests without root authority;basis=carries persistent identity into root-guarded cold retirement without treating old device numbers as filesystem identity file:Exec@938,file:Lstat@10711,file:OpenRoot@10633,file:Read@1290,registry:Addr@3565,registry:Addrs@3583,registry:Backing@3602,registry:Canary@3659,registry:ClaimIncarnation@3443,registry:ClaimIncarnation@3945,registry:ClaimOwner@11461,registry:ClaimOwner@4027,registry:ClaimOwner@4047,registry:ClaimOwner@5922,registry:ClaimOwner@7014,registry:ClaimOwner@8272,registry:ClaimOwnership@11053,registry:ClaimOwnership@3413,registry:ClaimOwnership@4391,registry:ClaimOwnership@4609,registry:ClaimOwnership@4642,registry:ClaimRowID@11351,registry:ClaimRowID@11432,registry:ClaimRowID@4065,registry:ClaimRowID@4673,registry:ClaimRowID@6973,registry:ClaimRowID@8297,registry:ClaimStamp@3488,registry:ClaimStamp@3992,registry:Host@6850,registry:Host@8234,registry:Kind@3521,registry:Name@4727,registry:Name@6865,registry:Name@8253,registry:Platform@3643,registry:Registration@4091,registry:Registration@6930,registry:Registration@8322,registry:Replica@3673,registry:Route@11053,registry:Route@4391,registry:Route@4642,registry:Route@5173,registry:Route@5922,registry:Timers@4590,registry:Timers@4609,registry:Timers@4673,registry:Timers@4727,registry:Timers@6802,registry:Timers@8109,registry:Tunnel@3623,registry:Unit@3547,whole:ClaimOwnership@11067,whole:ClaimOwnership@3417,whole:ClaimOwnership@4403,whole:ClaimOwnership@4625,whole:ClaimOwnership@4654,whole:ColdRows@11055,whole:ColdRows@12167,whole:ColdRows@12177,whole:ColdRows@4888,whole:ColdRows@4896,whole:ColdRows@4904,whole:ColdRows@4911,whole:ColdRows@5175,whole:ColdRows@5924,whole:ColdRows@6804,whole:ColdRows@8111,whole:Route@11061,whole:Route@3398,whole:Route@3758,whole:Route@4190,whole:Route@4198,whole:Route@4249,whole:Route@4257,whole:Route@4397,whole:Route@4648,whole:Route@5182,whole:Route@5931,whole:Timer@3929,whole:Timer@4203,whole:Timer@4211,whole:Timer@4262,whole:Timer@4300,whole:Timer@4596,whole:Timer@4615,whole:Timer@4679,whole:Timer@4733,whole:Timer@6811,whole:Timer@8118 57685ec5d96946512cbf1382e247e8881c564351db594d24d005264eccb03b6b |
| @@ -291,3 +291,9 @@ internal/host/claimstop_writers_linux.go rootInstallationCoverage authority C,G, |
| internal/host/claimunit.go claimUnitContent authority E1,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=adds write permission for exactly the public retry directory only when the root settings reader has verified a complete census and prepared that directory 3082c34490fbf6f95ebace69d97161993635c9cc1fee346641fdc61cb1f2e43f |
| internal/host/claimunit.go readClaimUnitSettings authority C,E3,G,J4 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=retains exact no-census unit settings; a claims-enabled complete census prepares the root-owned retry directory before the hardened controller starts data-literal@123,data-literal@1708,data-literal@1793 bb6950ff1fff6355dde999d17330e9cd29b49ce051b66bfe2c8d5bc05424d6a5 |
| internal/host/rootupdates_runtime.go prepareRootRetryUnitDirectory authority E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=root unit installer requires an independently opened complete census before creating the public retry directory or enabling its controller write path file:Lstat@67,file:Open@603 3be70325e77de790f369531610c5889da7af25a185fdde6062c49356ca1a9335 |
| +internal/caps/converge.go type Ops diagnostic H3 rev3=PROPOSED-p3;run=10;identity=compile-time capability operation shape;basis=adds root authorization callback shared by converge restore and restart file:Query@316 43fdc1addb312d7a6f70b140496a0d63731d29c2f41aa8b977b6509b46758c40 |
| +internal/caps/converge.go Converge authority E4,J4 rev3=PROPOSED-p3;run=10;identity=root adopt uid 0;basis=checks root authorization before any capability file or systemd effect; no-census legacy behavior retained 972b6fe7327fcfd33c9ad737546c6f7e1471686125aba81e7146204022029dd6 |
| +internal/caps/converge.go Restore authority E4,J4 rev3=PROPOSED-p3;run=10;identity=root adopt uid 0;basis=checks root authorization before rollback writes or unit verbs add26a1389d77a66bbc1e35ff92196210a0958c08f3607afecdd24912583bb71 |
| +internal/caps/converge.go Restart authority E4,J4 rev3=PROPOSED-p3;run=10;identity=root adopt uid 0;basis=checks root authorization before daemon restart e5f19c2deb81d90947a4759e787d7f2e8a04e20d52737e4078635622d5fd6536 |
| +@coverage internal/caps b964b8931e9d42939a419c2cf3041cac32eda07edc4fbaabdc4e37f56ddd8d01 |
| +internal/caps/rootauthority.go Ops.authorizeMutation authority E4,J4 rev3=PROPOSED-p3;run=10;identity=root adopt uid 0;basis=refuses legacy unit capability mutation on any retained census or unreadable census; root configuration stays fixed; absence preserves legacy operations file:Lstat@55 5549b2317e66dda41d73b23216bfca7b18ab1814607140794eb40124feae3618 |