carlos / platform Public

Plan 14 integrated: whole-branch review fixes P3-R9 (not for merge)

No-census proof minting and history as main; effective-unit verification for ordinary launches; activation preflight from verified records plus the 4a guard at binding; restricted recovery for pending inventory; floor escape through startup and guardRun; mandatory release exclusion for initial migration; reboot-safe prev identity; guard recovery as main for nonparticipants; census corrections.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 9326523704b9cd32739a540d07c08e10a5848fea parent 726a579
34 files changed, +1112 −86
  • internal/host/agent.go +9 −26
  • internal/host/claimactivation_preflight.go +74 −0
  • internal/host/claimactivation_preflight_test.go +36 −0
  • internal/host/claimconstruction.go +7 −0
  • internal/host/claimconstruction_rootrunner_linux_test.go +19 −0
  • internal/host/claimjournal_migration.go +5 −0
  • internal/host/claimreview_test.go +1 −0
  • internal/host/claimstop_run.go +19 −1
  • internal/host/claimunit_binding.go +6 −0
  • internal/host/claimunit_dispatch.go +9 −0
  • internal/host/claimunit_dropin_linux.go +2 −1
  • internal/host/claimunit_effective.go +39 −7
  • internal/host/claimunit_materialize.go +6 −0
  • internal/host/claimunit_production.go +9 −3
  • internal/host/claimunit_startup_gate_root_linux_test.go +0 −6
  • internal/host/claimunit_startup_test.go +8 −2
  • internal/host/guardexec.go +8 −1
  • internal/host/guardtransaction.go +9 −2
  • internal/host/plan14_installed_launch_root_linux_test.go +54 −2
  • internal/host/plan14_r9_root_linux_test.go +496 −0
  • internal/host/plan14_root_migration_test.go +4 −0
  • internal/host/proofroot.go +27 −1
  • internal/host/proofroot_test.go +26 −0
  • internal/host/roothistory_creation.go +1 −3
  • internal/host/rootinventory_startup.go +61 −0
  • internal/host/rootmigration.go +7 −0
  • internal/host/rootmigration_bootstrap_root_linux_test.go +5 −2
  • internal/host/rootmigration_production_linux.go +47 −1
  • internal/host/rootmigration_startup.go +18 −8
  • internal/host/rootmovesfloor_boot.go +8 −1
  • internal/host/rootmovesfloor_boot_test.go +29 −0
  • internal/host/rootreleaselock.go +19 −1
  • internal/host/systemapply.go +4 −1
  • internal/provision/testdata/plan14-root-reads-p3.tsv +40 −17

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	307.309s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.568s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.226s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.701s
ok  	github.com/carlosframework/platform/internal/activator	249.957s
ok  	github.com/carlosframework/platform/internal/agentsock	0.019s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.477s
ok  	github.com/carlosframework/platform/internal/apperr	0.090s
ok  	github.com/carlosframework/platform/internal/applog	0.139s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.754s
?   	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.014s
ok  	github.com/carlosframework/platform/internal/caps	0.020s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.697s
ok  	github.com/carlosframework/platform/internal/changelog	0.312s
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	391.713s
?   	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.120s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.011s
ok  	github.com/carlosframework/platform/internal/dbverify	0.009s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.009s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.398s
ok  	github.com/carlosframework/platform/internal/doorkey	0.012s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.811s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.010s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.140s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.019s
ok  	github.com/carlosframework/platform/internal/geo	0.011s
ok  	github.com/carlosframework/platform/internal/geoip	0.033s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.169s
ok  	github.com/carlosframework/platform/internal/host	175.310s
ok  	github.com/carlosframework/platform/internal/hostactors	0.550s
ok  	github.com/carlosframework/platform/internal/hostbox	2.642s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.566s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	4.832s
ok  	github.com/carlosframework/platform/internal/hostfence	0.038s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.485s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.022s
ok  	github.com/carlosframework/platform/internal/hostguard	1.035s
ok  	github.com/carlosframework/platform/internal/hostops	1.610s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.005s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.479s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.147s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.225s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.749s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.006s
ok  	github.com/carlosframework/platform/internal/layout	1.558s
ok  	github.com/carlosframework/platform/internal/lease	9.801s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.010s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.175s
ok  	github.com/carlosframework/platform/internal/maintenance	0.104s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.526s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.073s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	21.057s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.809s
ok  	github.com/carlosframework/platform/internal/objstore	0.191s
ok  	github.com/carlosframework/platform/internal/ops	0.175s
ok  	github.com/carlosframework/platform/internal/park	0.219s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	29.931s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.006s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.442s
ok  	github.com/carlosframework/platform/internal/relay	7.245s
ok  	github.com/carlosframework/platform/internal/release	22.073s
ok  	github.com/carlosframework/platform/internal/rollout	3.678s
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.063s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.574s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.514s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.499s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.826s
ok  	github.com/carlosframework/platform/internal/status	0.042s
ok  	github.com/carlosframework/platform/internal/steering	0.009s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.425s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.567s
=== 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: 48431266 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 05:06:13 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEEA2B4CC0A3D7, 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 2752041b6..b1c7db1b2 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4622,10 +4622,17 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// no-ops on the stopped unit, exits 0, and stamps a build that has never
// run). -stage-only keeps it for the same reason it keeps the converge:
// a relayed stage is exactly the request that performs such a restart.
- history, err := openRootHistory(store, release.OptDir())
+ pr := proofRoot{
+ obs: observer, reg: reg, store: store, bakeDir: dataDir(),
+ healthyDir: release.RootHealthyDir, policyPath: release.ProofPolicyPath,
+ readKey: readProvingKey, hourly: !*stageOnly, logf: log.Printf,
+ }
+ history, err := configureRootProof(&pr, restartChecks, func() (*release.RootHistoryPass, error) {
+ return openRootHistory(store, release.OptDir())
+ })
if err != nil {
log.Printf("adopt: %v", err)
- } else {
+ } else if history != nil {
defer history.History.Store.Close()
linkRootHistory(history, restartChecks, release.OptDir(), os.Getenv)
}
@@ -4654,30 +4661,6 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// adopt a proven head over it (final review H4). After it, mint on a
// proving box and, on the hourly pass, say when a requirement is off;
// a route the pass changed is looked at again first.
- pr := proofRoot{
- mintCensus: func() (bool, error) {
- legacy, err := restartChecks.allowLegacy()
- return !legacy && err == nil, err
- },
- obs: observer,
- reg: reg,
- store: store,
- bakeDir: dataDir(),
- healthyDir: release.RootHealthyDir,
- policyPath: release.ProofPolicyPath,
- readKey: readProvingKey,
- hourly: !*stageOnly,
- logf: log.Printf,
- }
- if history != nil {
- pr.recordProgram = func(b liveBuild) error { return recordRootProgram(history, b) }
- }
- pr.refusals = func(b release.HistoryBinding) ([]release.Refusal, error) {
- if history == nil {
- return nil, os.ErrPermission
- }
- return restartChecks.mintRefusals(history.History, b)
- }
passErr := pr.around(ctx, func() error {
return release.Pass(ctx, reg, store, pub, release.OptDir(), restart, passOpts...)
})
diff --git a/internal/host/claimactivation_preflight.go b/internal/host/claimactivation_preflight.go
index dac966d63..57e677860 100644
--- a/internal/host/claimactivation_preflight.go
+++ b/internal/host/claimactivation_preflight.go
@@ -1,12 +1,24 @@
package host
import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostops"
"github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "os"
+ "slices"
+ "strings"
)
const claimNamedUnit4aComplete = false
+var openClaimActivationState = rootstate.Open
+
func claimActivationPreflight(reg *registry.Registry) error {
if reg == nil {
return hostwrite.ErrUnavailable
@@ -25,3 +37,65 @@ func claimActivationPreflight(reg *registry.Registry) error {
}
return nil
}
+
+func claimActivationVerified(ctx context.Context, store release.Store, r ClaimRuntimeResources) error {
+ if claimNamedUnit4aComplete {
+ return nil
+ }
+ apps, err := hostops.Apps(ctx, store)
+ if err != nil {
+ return err
+ }
+ cfg := provisionTickConfig(os.Getenv)
+ for _, app := range apps {
+ records, err := hostops.Records(ctx, store, app.Account, app.Name)
+ if err != nil {
+ return err
+ }
+ for _, row := range records {
+ if row.Holder.Family != "instance" {
+ continue
+ }
+ var rec provision.Record
+ if json.Unmarshal(row.Body, &rec) != nil {
+ return hostwrite.ErrUnavailable
+ }
+ if rec.Region != "" && rec.Region != cfg.Region || rec.Pool != "" && !slices.Contains(cfg.Pools, rec.Pool) {
+ continue
+ }
+ if rec.Unit != "" {
+ return hostwrite.ErrUnavailable
+ }
+ }
+ }
+ if r.Mode == "agent" {
+ return nil
+ }
+ shapes, err := productionClaimUnitShapes()
+ if err != nil {
+ return err
+ }
+ if len(shapes) != 0 {
+ return hostwrite.ErrUnavailable
+ }
+ for _, dir := range []string{"/var/lib/carlos-claims/unit-bindings", "/var/lib/carlos-claims/unit-materializations"} {
+ state, err := openClaimActivationState(dir, false, false)
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ if err != nil {
+ return err
+ }
+ names, err := state.Names()
+ state.Close()
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ if !strings.HasSuffix(name, ".lock") {
+ return hostwrite.ErrUnavailable
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/host/claimactivation_preflight_test.go b/internal/host/claimactivation_preflight_test.go
index 19bbaf85b..4feb386ac 100644
--- a/internal/host/claimactivation_preflight_test.go
+++ b/internal/host/claimactivation_preflight_test.go
@@ -3,13 +3,16 @@ package host
import (
"context"
"errors"
+ "github.com/carlosframework/platform/internal/provision"
"os"
"path/filepath"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
)
type activationPreflightOwner struct{ calls int }
@@ -20,6 +23,11 @@ func (p *activationPreflightOwner) OpenClaimOwners(context.Context, release.Stor
}
func TestPlan14ActivationNamedUnitPreflight(t *testing.T) {
+ oldOpen, oldShapes := openClaimActivationState, productionClaimUnitShapes
+ t.Cleanup(func() { openClaimActivationState, productionClaimUnitShapes = oldOpen, oldShapes })
+ openClaimActivationState = func(string, bool, bool) (rootstate.Store, error) { return nil, os.ErrNotExist }
+ productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
+
for _, mode := range []string{"agent", "adopt", "instance-control"} {
for _, kind := range []string{"named", "backing", "ordinary", "empty"} {
t.Run(mode+"/"+kind, func(t *testing.T) {
@@ -71,3 +79,31 @@ func TestPlan14ActivationNamedUnitPreflight(t *testing.T) {
}
}
}
+
+func TestPlan14ActivationRejectsHiddenNamedRecords(t *testing.T) {
+ for _, attack := range []string{"omitted", "cleared"} {
+ t.Run(attack, func(t *testing.T) {
+ t.Setenv("CARLOS_REGION", "")
+ store := release.NewDirStore(t.TempDir())
+ plan14PutRecord(t, store, provision.Record{Schema: 1, Host: "console.example", Account: "bab", App: "console", Channel: "stable", Unit: "console.service", CreatedAt: time.Now()})
+ reg := restartTestRegistry(t)
+ if attack == "cleared" {
+ if err := reg.Upsert(registry.Route{Host: "console.example", Account: "bab", App: "console", Socket: "/run/console.sock"}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, mode := range []string{"agent", "instance-control"} {
+ p := &activationPreflightOwner{}
+ dir := t.TempDir()
+ r := ClaimRuntimeResources{Mode: mode, Registry: reg, DataDir: dir, OptDir: dir, UnitsDir: dir}
+ if _, err := OpenProductionClaimAssembly(t.Context(), store, r, p); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 {
+ t.Fatal("hidden named route entered construction", err, p.calls)
+ }
+ r.Mode = "instance-control"
+ if err := migrateClaimJournalsWith(t.Context(), store, r, p, true); !errors.Is(err, hostwrite.ErrUnavailable) || p.calls != 0 {
+ t.Fatal("hidden named route entered migration", err, p.calls)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/host/claimconstruction.go b/internal/host/claimconstruction.go
index 69dbff2b0..4ae846a22 100644
--- a/internal/host/claimconstruction.go
+++ b/internal/host/claimconstruction.go
@@ -146,6 +146,9 @@ func OpenProductionClaimAssembly(ctx context.Context, store release.Store, r Cla
if err := claimActivationPreflight(r.Registry); err != nil {
return nil, err
}
+ if err := claimActivationVerified(ctx, store, r); err != nil {
+ return nil, err
+ }
owners, err := provider.OpenClaimOwners(ctx, store, r)
if err != nil {
return nil, err
@@ -225,6 +228,10 @@ func OpenProductionClaimAssembly(ctx context.Context, store release.Store, r Cla
return nil, errors.Join(refreshErr, hostwrite.ErrUnavailable)
}
actor = fresh
+ if err := recoverRootInventoryStartup(ctx, store, r, owners, actor, journals, launches); err != nil {
+ launches.Close()
+ return nil, err
+ }
if err := verifyRootMigrationStartup(ctx, rootFloorIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment}, journals.Inventory); err != nil {
launches.Close()
return nil, err
diff --git a/internal/host/claimconstruction_rootrunner_linux_test.go b/internal/host/claimconstruction_rootrunner_linux_test.go
index f5cff1030..745079928 100644
--- a/internal/host/claimconstruction_rootrunner_linux_test.go
+++ b/internal/host/claimconstruction_rootrunner_linux_test.go
@@ -15,6 +15,7 @@ import (
"os/user"
"path/filepath"
"strconv"
+ "syscall"
"testing"
"time"
@@ -72,6 +73,16 @@ func filesSeedInstanceControlChild(t *testing.T, binary, root string, listener *
}
cmd := exec.Command(binary, "-test.run=^TestRootFilesSeedInstanceControlPayload$", "-test.v", "-test.timeout=90s")
cmd.Env = append(os.Environ(), "FILES_SEED_CONTROL=1", "TMPDIR="+root)
+ if os.Getenv("PLAN14_R9_PENDING") != "" {
+ cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWNS}
+ for _, path := range []string{"/run/carlos", "/run/carlos/hostguard"} {
+ if err := os.Mkdir(path, 0755); err == nil {
+ t.Cleanup(func() { os.RemoveAll(path) })
+ } else if !os.IsExist(err) {
+ t.Fatal(err)
+ }
+ }
+ }
cmd.ExtraFiles = []*os.File{file}
cmd.Stdout, cmd.Stderr = output, output
if err := cmd.Start(); err != nil {
@@ -138,6 +149,14 @@ func TestRootFilesSeedInstanceControlPayload(t *testing.T) {
if rootLaunchStartup.err != nil {
t.Fatal(rootLaunchStartup.err)
}
+ if mode := os.Getenv("PLAN14_R9_PENDING"); mode != "" {
+ filesSeedPendingConstruction(t, release.NewDirStore(dir), resources, owners.ClaimOwnerBindings, actor)
+ if mode == "system" {
+ if err := recoverRootExecution(t.Context(), "/data", systemGuardDir(resources.OptDir)); err != nil {
+ t.Fatal("production system recovery", err)
+ }
+ }
+ }
RunInstanceControl(nil)
t.Fatal("claims-on instance-control returned before serving")
}
diff --git a/internal/host/claimjournal_migration.go b/internal/host/claimjournal_migration.go
index 802ea3685..edd9659db 100644
--- a/internal/host/claimjournal_migration.go
+++ b/internal/host/claimjournal_migration.go
@@ -70,6 +70,11 @@ func migrateClaimJournalsWith(ctx context.Context, store release.Store, r ClaimR
return err
}
}
+ if census {
+ if err := claimActivationVerified(ctx, store, r); err != nil {
+ return err
+ }
+ }
owners, err := provider.OpenClaimOwners(ctx, store, r)
if err != nil {
return err
diff --git a/internal/host/claimreview_test.go b/internal/host/claimreview_test.go
index 1548d48ee..d30d0be44 100644
--- a/internal/host/claimreview_test.go
+++ b/internal/host/claimreview_test.go
@@ -149,6 +149,7 @@ func TestClaimReviewSystemStageUsesLiveControlSettings(t *testing.T) {
}
func TestClaimReviewControlEntryKeepsServiceLifetime(t *testing.T) {
+ fixtureWithoutRootMigrationCensus(t)
service, cancel := context.WithCancel(t.Context())
defer cancel()
var startup context.Context
diff --git a/internal/host/claimstop_run.go b/internal/host/claimstop_run.go
index 874042949..304add7e6 100644
--- a/internal/host/claimstop_run.go
+++ b/internal/host/claimstop_run.go
@@ -23,7 +23,7 @@ func RunInstanceControl(args []string) {
log.Fatal(err)
}
if hostwrite.Enabled(os.Getenv) {
- if err := claimActivationReady(ctx, rootparticipation.OpenForUpdate); err != nil {
+ if err := claimRecoveryReady(rootparticipation.OpenForUpdate); err != nil {
log.Print(&ClaimDependencyError{Contracts: []string{"census", "registered verifier", "flag must stay off until Part 3 (G)"}})
return
}
@@ -84,6 +84,9 @@ func RunInstanceControl(args []string) {
if err := recovery.RecoverPending(startup); err != nil {
log.Print(err)
}
+ if err := claimActivationReady(startup, rootparticipation.OpenForUpdate); err != nil {
+ log.Fatal(err)
+ }
finishStartup()
lifecycle, ok := handler.(interface{ Start(context.Context) })
if !ok {
@@ -145,3 +148,18 @@ func claimActivationReady(ctx context.Context, open func() (rootparticipation.Le
defer cancel()
return l.WithLaunchExclusion(ctx, l.ActivationReady)
}
+
+func claimRecoveryReady(open func() (rootparticipation.Ledger, bool, error)) error {
+ l, required, err := open()
+ if l.State != nil {
+ defer l.State.Close()
+ }
+ if err != nil {
+ return err
+ }
+ if !required {
+ return rootparticipation.ErrUnavailable
+ }
+ _, err = l.RecoveryCensus()
+ return err
+}
diff --git a/internal/host/claimunit_binding.go b/internal/host/claimunit_binding.go
index bc4690d6c..5ab79797d 100644
--- a/internal/host/claimunit_binding.go
+++ b/internal/host/claimunit_binding.go
@@ -34,6 +34,7 @@ type claimUnitBinding struct {
}
type claimUnitBindings struct {
+ admit func() error
handover func(context.Context, claimUnitBinding, claimUnitBinding) error
state rootstate.Store
installation func(context.Context, string) (hostguard.RootInstallation, error)
@@ -142,6 +143,11 @@ func (s *claimUnitBindings) current(ctx context.Context, host string) (claimUnit
}
func (s *claimUnitBindings) establish(ctx context.Context, host string, resolved ...claimUnitBinding) (claimUnitBinding, error) {
+ if s.admit != nil {
+ if err := s.admit(); err != nil {
+ return claimUnitBinding{}, err
+ }
+ }
if s == nil || s.state == nil {
return claimUnitBinding{}, os.ErrPermission
}
diff --git a/internal/host/claimunit_dispatch.go b/internal/host/claimunit_dispatch.go
index a72e23780..1b2efe0b7 100644
--- a/internal/host/claimunit_dispatch.go
+++ b/internal/host/claimunit_dispatch.go
@@ -42,11 +42,20 @@ func (d claimUnitDispatch) forStart(ctx context.Context, host string) (instancec
}
if u.BoundUnit == "" {
shapes, e := d.shapes()
+ if e != nil {
+ return u, e
+ }
if e == nil {
if _, configured := shapes[host]; configured {
return u, os.ErrPermission
}
}
+ if d.effective == nil {
+ return u, os.ErrPermission
+ }
+ if err := d.effective(ctx, claimUnitShape{Host: host, Unit: instancectl.UnitName(instancectl.RoleRun, host)}); err != nil {
+ return u, err
+ }
}
if u.BoundUnit != "" {
shapes, err := d.shapes()
diff --git a/internal/host/claimunit_dropin_linux.go b/internal/host/claimunit_dropin_linux.go
index 30e0265c7..4bbbdc7ca 100644
--- a/internal/host/claimunit_dropin_linux.go
+++ b/internal/host/claimunit_dropin_linux.go
@@ -9,7 +9,8 @@ import (
)
func openClaimUnitDropin(unit string) (*os.File, error) {
- if !instancectl.ValidBoundClaimUnit(unit) {
+ role, _, ordinary := instancectl.ParseUnitName(unit)
+ if !instancectl.ValidBoundClaimUnit(unit) && !(ordinary && role == instancectl.RoleRun) {
return nil, os.ErrPermission
}
const base = "/run/systemd/system"
diff --git a/internal/host/claimunit_effective.go b/internal/host/claimunit_effective.go
index 727e9df20..48bb29708 100644
--- a/internal/host/claimunit_effective.go
+++ b/internal/host/claimunit_effective.go
@@ -18,7 +18,11 @@ import (
func claimReservedDropin(s claimUnitShape) []byte {
source := filepath.Join("/run/carlos-control/exe/.claim", provision.InstanceID(s.Host))
- return []byte("[Service]\nBindReadOnlyPaths=" + source + "/launch.json:" + filepath.Join(provision.InstanceDir("/data", s.Host), instancectl.LaunchFileName(instancectl.RoleRun)) + "\nBindReadOnlyPaths=" + source + "/exec.sock:" + instancectl.ClaimExecSocket + "\nBindReadOnlyPaths=" + source + "/permit.json:" + instancectl.ClaimExecSnapshot + "\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:" + instancectl.ClaimExecBootID + "\nExecStart=\nExecStart=" + instancectl.SystemBinary + " instance-exec -claim-reservation=" + instancectl.ClaimExecSnapshot + " run " + provision.InstanceID(s.Host) + "\n")
+ instance := provision.InstanceID(s.Host)
+ if s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host) {
+ instance = "%I"
+ }
+ return []byte("[Service]\nBindReadOnlyPaths=" + source + "/launch.json:" + filepath.Join(provision.InstanceDir("/data", s.Host), instancectl.LaunchFileName(instancectl.RoleRun)) + "\nBindReadOnlyPaths=" + source + "/exec.sock:" + instancectl.ClaimExecSocket + "\nBindReadOnlyPaths=" + source + "/permit.json:" + instancectl.ClaimExecSnapshot + "\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:" + instancectl.ClaimExecBootID + "\nExecStart=\nExecStart=" + instancectl.SystemBinary + " instance-exec -claim-reservation=" + instancectl.ClaimExecSnapshot + " run " + instance + "\n")
}
type claimEffectiveRefusal struct {
@@ -33,6 +37,35 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
if ctl == nil {
return claimEffectiveRefusal{"unknown", os.ErrPermission}
}
+ ordinary := s.Unit == instancectl.UnitName(instancectl.RoleRun, s.Host)
+ fragment := "/etc/systemd/system/" + s.Unit
+ var definition []byte
+ if ordinary {
+ fragment = "/etc/systemd/system/carlos-instance@.service"
+ var err error
+ definition, err = unitsFS.ReadFile("units/carlos-instance@.service")
+ if err != nil {
+ return err
+ }
+ f, err := rootstate.OpenTrustedFile(fragment)
+ if err != nil {
+ return err
+ }
+ body, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1))
+ f.Close()
+ if err != nil {
+ return err
+ }
+ if !bytes.Equal(body, definition) {
+ return os.ErrPermission
+ }
+ } else {
+ var err error
+ definition, err = claimBoundUnitDefinition(s)
+ if err != nil {
+ return err
+ }
+ }
scalar := map[string]string{"User": "carlos", "Group": "carlos", "NoNewPrivileges": "yes", "ProtectSystem": "strict", "ProtectHome": "yes", "PrivateTmp": "yes", "PrivateDevices": "yes", "PrivateIPC": "yes", "ProtectControlGroups": "yes", "ProtectProc": "invisible", "ProcSubset": "pid", "PrivatePIDs": "yes", "RestrictNamespaces": "yes", "CapabilityBoundingSet": "", "MemoryDenyWriteExecute": "yes", "ProtectKernelTunables": "yes", "ProtectKernelModules": "yes", "ProtectKernelLogs": "yes", "ProtectClock": "yes", "ProtectHostname": "yes", "RemoveIPC": "yes", "LockPersonality": "yes", "RestrictRealtime": "yes", "Restart": "no", "Slice": instancectl.SliceName(s.Host)}
if s.Addr != "" {
host, port, e := net.SplitHostPort(s.Addr)
@@ -69,7 +102,7 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
return claimEffectiveRefusal{"missing:" + key, os.ErrPermission}
}
}
- if props["FragmentPath"] != "/etc/systemd/system/"+s.Unit || props["NeedDaemonReload"] != "no" {
+ if props["FragmentPath"] != fragment || props["NeedDaemonReload"] != "no" {
return claimEffectiveRefusal{"unknown", os.ErrPermission}
}
for key, want := range scalar {
@@ -77,10 +110,13 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
return claimEffectiveRefusal{"value:" + key + "=" + props[key], os.ErrPermission}
}
}
- if strings.Contains(props["BindsTo"], "carlos-edge.service") || strings.Contains(props["PartOf"], "carlos-edge.service") {
+ if !ordinary && (strings.Contains(props["BindsTo"], "carlos-edge.service") || strings.Contains(props["PartOf"], "carlos-edge.service")) {
return claimEffectiveRefusal{"edge-coupling", os.ErrPermission}
}
executable := "/usr/bin/false"
+ if ordinary {
+ executable = instancectl.SystemBinary
+ }
if props["DropInPaths"] != "" {
path := filepath.Join("/run/systemd/system", s.Unit+".d", "60-claim-launch.conf")
if props["DropInPaths"] != path {
@@ -103,10 +139,6 @@ func claimEffectiveUnit(ctx context.Context, s claimUnitShape, ctl func(context.
if strings.Count(props["ExecStart"], "path=") != 1 || !strings.Contains(props["ExecStart"], "path="+executable+" ;") {
return claimEffectiveRefusal{"unknown", os.ErrPermission}
}
- definition, err := claimBoundUnitDefinition(s)
- if err != nil {
- return err
- }
for _, key := range []string{"IPAddressAllow", "IPAddressDeny"} {
want := ""
for _, line := range strings.Split(string(definition), "\n") {
diff --git a/internal/host/claimunit_materialize.go b/internal/host/claimunit_materialize.go
index 713521fb6..5f8f1a616 100644
--- a/internal/host/claimunit_materialize.go
+++ b/internal/host/claimunit_materialize.go
@@ -24,6 +24,7 @@ type claimUnitMaterialization struct {
}
type claimUnitMaterializer struct {
+ admit func() error
socketCapture func(context.Context, claimUnitShape, claimUnitPredecessor) (claimUnitSocketIdentity, error)
socketPark func(claimUnitBinding, claimUnitSocketIdentity) error
predecessor func(context.Context, claimUnitBinding) (claimUnitPredecessor, error)
@@ -77,6 +78,11 @@ func claimRetiredUnitDefinition(b claimUnitBinding) []byte {
}
func (m claimUnitMaterializer) install(ctx context.Context, b claimUnitBinding) error {
+ if m.admit != nil {
+ if err := m.admit(); err != nil {
+ return err
+ }
+ }
if m.state == nil || m.read == nil || m.create == nil || m.idle == nil || !b.valid() || b.Retiring || b.Retired {
return os.ErrPermission
}
diff --git a/internal/host/claimunit_production.go b/internal/host/claimunit_production.go
index ae5bc833e..a049a5ccc 100644
--- a/internal/host/claimunit_production.go
+++ b/internal/host/claimunit_production.go
@@ -49,7 +49,13 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl
if err != nil {
return err
}
- b := &claimUnitBindings{state: state}
+ admit := func() error {
+ if !claimNamedUnit4aComplete {
+ return hostbox.ErrUnsupported
+ }
+ return nil
+ }
+ b := &claimUnitBindings{state: state, admit: admit}
b.canonical = func(ctx context.Context, host string) (claimUnitShape, error) {
current, err := productionClaimUnitShapes()
if err != nil {
@@ -89,7 +95,7 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl
return err
}
files := claimUnitFiles{dir: "/etc/systemd/system"}
- material := claimUnitMaterializer{idle: func(host string) error {
+ material := claimUnitMaterializer{admit: admit, idle: func(host string) error {
binding, err := b.read(host)
if err != nil {
return err
@@ -181,7 +187,7 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl
shape := shapes[host]
// Named services must keep their operator definition until a reserved
// boot/adoption controller can keep them running independently of edge.
- if shape.Backing == "" {
+ if !claimNamedUnit4aComplete || shape.Backing == "" {
log.Print(hostbox.ErrUnsupported)
continue
}
diff --git a/internal/host/claimunit_startup_gate_root_linux_test.go b/internal/host/claimunit_startup_gate_root_linux_test.go
index ff7f9f5b8..413412125 100644
--- a/internal/host/claimunit_startup_gate_root_linux_test.go
+++ b/internal/host/claimunit_startup_gate_root_linux_test.go
@@ -47,12 +47,6 @@ func plan14ProductionStartupDefersReplacement(t *testing.T, original *ClaimLaunc
if err := bindProductionClaimUnits(t.Context(), &wrapped, assembly); err != nil {
t.Fatal("named unit blocked control startup", err)
}
- if binding.Shape.Backing == "unit" {
- if err := material.verify(binding); err != nil {
- t.Fatal("named startup gate blocked backing-unit materialization", err)
- }
- return
- }
after, afterID, err := material.read(binding.Shape.Unit)
if err != nil || !bytes.Equal(before, after) || !id.Same(afterID) {
t.Fatal("startup replaced named unit before reserved startup works", err)
diff --git a/internal/host/claimunit_startup_test.go b/internal/host/claimunit_startup_test.go
index 675df560d..629c13e27 100644
--- a/internal/host/claimunit_startup_test.go
+++ b/internal/host/claimunit_startup_test.go
@@ -24,8 +24,14 @@ func TestPlan14UnitStartupIsolation(t *testing.T) {
if _, err := d.forStart(t.Context(), in.Host); err == nil {
t.Fatal("corrupt config launched named unit")
}
- if _, err := d.forStart(t.Context(), "sibling.example"); err != nil {
- t.Fatal("corrupt named config blocked ordinary launch", err)
+ if _, err := d.forStart(t.Context(), "sibling.example"); err == nil {
+ t.Fatal("corrupt config allowed an unverified ordinary launch")
+ }
+ d.shapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
+ checked := false
+ d.effective = func(context.Context, claimUnitShape) error { checked = true; return nil }
+ if _, err := d.forStart(t.Context(), "sibling.example"); err != nil || !checked {
+ t.Fatal("verified ordinary launch refused", err, checked)
}
}
diff --git a/internal/host/guardexec.go b/internal/host/guardexec.go
index 30f38ad31..862940e90 100644
--- a/internal/host/guardexec.go
+++ b/internal/host/guardexec.go
@@ -46,7 +46,14 @@ func RunSystemGuardExec(args []string) {
return
}
}
- if policy.Mode != "unpublished" && args[0] != "recover" {
+ networkFree := false
+ if args[0] == "recover" {
+ _, networkFree, err = rootMovesFloorIdentity()
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+ if policy.Mode != "unpublished" && !networkFree {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
store, pub, e := openSystemStore(ctx)
if e == nil {
diff --git a/internal/host/guardtransaction.go b/internal/host/guardtransaction.go
index 5ef358844..3a2dacc3b 100644
--- a/internal/host/guardtransaction.go
+++ b/internal/host/guardtransaction.go
@@ -358,7 +358,7 @@ func (a *systemApplier) closeGuardJournal(ctx context.Context) error {
return a.endGuard(ctx)
}
func (a *systemApplier) guardRun(ctx context.Context, wait time.Duration, name string, args ...string) (string, error) {
- if _, required, err := a.rootMoves.read(); err != nil {
+ if id, required, err := a.rootMoves.read(); err != nil {
return "", err
} else if required {
binary, err := readRootFloorBinary(a.optDir, name)
@@ -370,7 +370,14 @@ func (a *systemApplier) guardRun(ctx context.Context, wait time.Duration, name s
return "", err
}
if !binary.supports(floor) {
- return "", errRootFloorHistory
+ repair := name == systemBinaryPath(a.optDir) || name == filepath.Join(systemGuardDir(a.optDir), "carlos")
+ if name == systemPrevPath(a.optDir) && rootFloorAllowsPrevIdentity(a.optDir, id) == nil {
+ repair = true
+ }
+ if !binary.supports(nil) || !repair {
+ return "", errRootFloorHistory
+ }
+ rootFloorAlert()
}
}
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go
index f10cd6d1f..93ca3fbc7 100644
--- a/internal/host/plan14_installed_launch_root_linux_test.go
+++ b/internal/host/plan14_installed_launch_root_linux_test.go
@@ -142,7 +142,15 @@ func TestPlan14NamedStartupDefersReplacementRootRunner(t *testing.T) {
}
}
+func TestPlan14OrdinaryUnitAuthorizationRootRunner(t *testing.T) {
+ plan14InstalledClaimLaunch(t, "ordinary-verified")
+}
+
func plan14InstalledClaimLaunch(t *testing.T, boundUnit string, runtimeUpdate ...bool) {
+ verifiedOrdinary := boundUnit == "ordinary-verified"
+ if verifiedOrdinary {
+ boundUnit = ""
+ }
woodstar := strings.HasPrefix(boundUnit, "woodstar:")
boundUnit = strings.TrimPrefix(boundUnit, "woodstar:")
deferReplacement := strings.HasPrefix(boundUnit, "defer:")
@@ -263,9 +271,23 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string, runtimeUpdate ..
t.Fatal(err)
}
}
- if materialized {
+ if materialized || verifiedOrdinary {
plan14MaterializedDependencies(t, ctl)
}
+ if verifiedOrdinary {
+ path := "/etc/systemd/system/carlos-edge.service"
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("existing edge", err)
+ }
+ if err := os.WriteFile(path, []byte("[Service]\nType=oneshot\nExecStart=/usr/bin/true\nRemainAfterExit=yes\n"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ ctl(context.Background(), "stop", "carlos-edge.service")
+ os.Remove(path)
+ ctl(context.Background(), "daemon-reload")
+ })
+ }
template, err := unitsFS.ReadFile("units/carlos-instance@.service")
if err != nil {
t.Fatal(err)
@@ -290,6 +312,10 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string, runtimeUpdate ..
}
unit := instancectl.UnitName(instancectl.RoleRun, host)
unitPath := "/run/systemd/system/carlos-instance@.service"
+ if verifiedOrdinary {
+ unitPath = "/etc/systemd/system/carlos-instance@.service"
+ lines = strings.Split(string(template), "\n")
+ }
if boundUnit != "" {
unit = boundUnit
unitPath = filepath.Join("/run/systemd/system", unit)
@@ -463,6 +489,31 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string, runtimeUpdate ..
}
return ""
}(), Systemctl: ctl, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data", WaitBudget: 20 * time.Second}
+ if verifiedOrdinary {
+ oldOpen, oldMaterial, oldShapes := openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes
+ defer func() {
+ openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes = oldOpen, oldMaterial, oldShapes
+ }()
+ openClaimUnitBindings = func() (rootstate.Store, error) {
+ return rootstate.Open(filepath.Join(dir, "ordinary-bindings"), true, false)
+ }
+ openClaimUnitMaterializations = func() (rootstate.Store, error) {
+ return rootstate.Open(filepath.Join(dir, "ordinary-materials"), true, false)
+ }
+ productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
+ wrapped := ClaimRootWithLaunch{Launch: f.root}
+ wrapped.Root.Stop.Coordinator.Kernel = claimUnitStopKernel{Units: f.root.Units}
+ assembly := &ClaimAssembly{}
+ if err := bindProductionClaimUnits(t.Context(), &wrapped, assembly); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { assembly.Close(context.Background()) })
+ dispatch := f.root.Units.(claimUnitDispatch)
+ plan14BoundOverrideRefusals(t, dispatch, host, unit, ctl)
+ if _, err := dispatch.forStart(t.Context(), host); err != nil {
+ t.Fatalf("ordinary authorized configuration: %#v", err)
+ }
+ }
var material *claimUnitMaterializer
if materialized {
ms, err := rootstate.Open(filepath.Join(dir, "unit-materializations"), true, false)
@@ -979,7 +1030,8 @@ func plan14BoundOverrideRefusals(t *testing.T, dispatch claimUnitDispatch, host,
for _, attack := range []struct{ name, body string }{
{"90-operator.conf", "[Service]\nExecStart=\nExecStart=/usr/bin/true\n"},
{"90-user.conf", "[Service]\nUser=root\n"},
- {"10-carlos-capabilities.conf", "[Service]\nIPAddressAllow=any\n"},
+ {"10-carlos-capabilities.conf", "[Service]\nCapabilityBoundingSet=CAP_SYS_ADMIN\n"},
+ {"90-mount.conf", "[Service]\nBindPaths=/etc:/tenant-secrets\n"},
} {
path := filepath.Join(dir, attack.name)
if err := os.WriteFile(path, []byte(attack.body), 0644); err != nil {
diff --git a/internal/host/plan14_r9_root_linux_test.go b/internal/host/plan14_r9_root_linux_test.go
new file mode 100644
index 000000000..33fa26bc3
--- /dev/null
+++ b/internal/host/plan14_r9_root_linux_test.go
@@ -0,0 +1,496 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "golang.org/x/sys/unix"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "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/moves"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func init() {
+ if os.Getenv("PLAN14_R9_LEGACY_MUTATOR") == "1" {
+ os.Stdout.Write([]byte("ready\n"))
+ var b [1]byte
+ os.Stdin.Read(b[:])
+ os.Exit(0)
+ }
+}
+
+func TestPlan14InitialMigrationExclusionRootRunner(t *testing.T) {
+ testRootMigrationBootstrap(t, false, false)
+}
+
+func plan14InitialMigrationExclusion(t *testing.T, store release.Store, r ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor, journals *hostbox.JournalService, launches *hostbox.LaunchJournal) {
+ for _, args := range [][]string{{"adopt"}, {"system", "apply"}} {
+ cmd := exec.Command(os.Args[0], args...)
+ cmd.Env = append(os.Environ(), "PLAN14_R9_LEGACY_MUTATOR=1")
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { stdin.Close(); cmd.Process.Kill(); cmd.Wait() })
+ var b [6]byte
+ if _, err := stdout.Read(b[:]); err != nil {
+ t.Fatal(err)
+ }
+ if err := migrateRootClaimAuthority(t.Context(), store, r, owners, actor, journals, launches); err == nil {
+ t.Fatal("migration overlapped legacy mutator", args)
+ }
+ if _, err := os.Stat(filepath.Join(rootparticipation.Namespace, "census.json")); !os.IsNotExist(err) {
+ t.Fatal("census created before quiescence", err)
+ }
+ stdin.Close()
+ cmd.Wait()
+ }
+ old := rootMigrationQuiesce
+ defer func() { rootMigrationQuiesce = old }()
+ rootMigrationQuiesce = func(context.Context) (func(), error) {
+ for _, path := range []string{"adopt", "update"} {
+ ctx, cancel := context.WithTimeout(t.Context(), 40*time.Millisecond)
+ ran := false
+ var err error
+ if path == "adopt" {
+ err = withClaimRootRelease(ctx, false, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) }, func() error { ran = true; return nil }, func() (rootparticipation.Ledger, bool, error) { return rootparticipation.Ledger{}, false, nil })
+ } else {
+ err = withExistingRootRelease(ctx, func() error { ran = true; return nil })
+ }
+ cancel()
+ if err == nil || ran {
+ t.Fatal("initial migration did not exclude", path, err)
+ }
+ }
+ return nil, os.ErrPermission
+ }
+ if err := migrateRootClaimAuthority(t.Context(), store, r, owners, actor, journals, launches); !errors.Is(err, os.ErrPermission) {
+ t.Fatal("exclusion witness", err)
+ }
+}
+
+func TestPlan14RepairExecutorEndToEndRootRunner(t *testing.T) {
+ opt := generationRootRunnerDir(t)
+ pub, key, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
+ id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"}
+ f, err := openRootMovesFloors(opt, id, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.close()
+ bound := moves.Floor{Min: 99, MinRelease: "v99.0.0"}
+ if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &bound, Source: "raised bound", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
+ t.Fatal(err)
+ }
+ for _, path := range []string{systemBinaryPath(opt), systemPrevPath(opt), filepath.Join(systemGuardDir(opt), "carlos"), filepath.Join(systemRootDir(opt), "staging", "carlos")} {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ rootFloorSignRunnerBytes(t, opt, path, "v2.0.0", []byte("#!/bin/sh\nprintf 'v2.0.0\\n'\n"), key)
+ }
+ if err := verifyRootMigrationFloors(opt, id); err != nil {
+ t.Fatal("repair controller cannot start", err)
+ }
+ a := systemApplier{optDir: opt, dataDir: t.TempDir(), rootMoves: func() (rootFloorIdentity, bool, error) { return id, true, nil }, ops: productionSysOps()}
+ for _, path := range []string{systemBinaryPath(opt), filepath.Join(systemGuardDir(opt), "carlos")} {
+ if out, err := a.guardRun(t.Context(), time.Second, path, "version"); err != nil || out == "" {
+ t.Fatal("certified helper blocked", out, err)
+ }
+ }
+ if _, err := a.guardRun(t.Context(), time.Second, filepath.Join(systemRootDir(opt), "staging", "carlos"), "version"); err == nil {
+ t.Fatal("below-floor replacement executed")
+ }
+ if err := a.floorAllowsForward("v2.0.0"); err != nil {
+ t.Fatal("installed repair executor cannot finish recovery", err)
+ }
+ j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(opt, p) }, idle: func(context.Context) error { return nil }}
+ if err := j.judge(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ var v rootFloorVerdict
+ raw, err := f.public.Read("prev.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := json.Unmarshal(raw, &v); err != nil {
+ t.Fatal(err)
+ }
+ v.Prev.File.Device++
+ raw, _ = json.Marshal(v)
+ if err := f.public.Write("prev.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v3.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
+ selected, err := rootFloorBootCandidate(t.Context(), j, true, func(context.Context, string) error { return nil }, func(context.Context) error { return nil })
+ if err != nil || selected != systemPrevPath(opt) {
+ t.Fatal("renumbered signed prev lost recovery", selected, err)
+ }
+ if out, err := a.guardRun(t.Context(), time.Second, selected, "version"); err != nil || out == "" {
+ t.Fatal("selected recovery image failed", out, err)
+ }
+ for _, recovery := range []bool{false, true} {
+ f := newApplyFixture(t, "canary", "v2.0.0")
+ f.a.optDir = opt
+ f.a.rootMoves = a.rootMoves
+ f.a.ops.run = productionSysOps().run
+ f.edgeStartedThisBoot = false
+ rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v2.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
+ prevDir := filepath.Join(f.a.stagingDir(), "prev")
+ unitDir := filepath.Join(prevDir, "etc", "systemd", "system")
+ if err := os.MkdirAll(unitDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(unitDir, "carlos-edge.service"), []byte("[Unit]\n"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(prevDir, "etc", "tmpfiles.d"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(prevDir, "etc", "tmpfiles.d", "carlos.conf"), nil, 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeJSONDurable(filepath.Join(prevDir, "plan.json"), bootstrapPlan{}, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeJSONDurable(f.a.file("journal.json"), systemJournal{Phase: systemPhaseInstalled, Target: "v2.0.0", PrevBuild: "v2.0.0", StartedAt: f.now}, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.a.Run(t.Context(), recovery); err != nil {
+ t.Fatalf("production recovery=%v: %v", recovery, err)
+ }
+ if journal, err := readSystemJournal(opt); err != nil || journal != nil {
+ t.Fatal("repair journal not closed", journal, err)
+ }
+ if result := f.result(); result == nil || result.Outcome != systemOutcomeRolledBack {
+ t.Fatal("repair result", result)
+ }
+ if out, err := f.a.guardRun(t.Context(), time.Second, systemBinaryPath(opt), "version"); err != nil || out == "" {
+ t.Fatal("repaired current cannot execute", out, err)
+ }
+ }
+}
+
+func TestPlan14PendingInventoryStartupRootRunner(t *testing.T) {
+ for _, mode := range []string{"instance-control", "system"} {
+ t.Run(mode, func(t *testing.T) {
+ t.Setenv("PLAN14_R9_PENDING", mode)
+ runServingParkPeerNamespace(t, false, false, "seed-construction")
+ })
+ }
+}
+
+type r9PendingBoundary struct {
+ rootstate.Store
+ failed bool
+}
+
+func (s *r9PendingBoundary) Write(name string, raw []byte) error {
+ if err := s.Store.Write(name, raw); err != nil {
+ return err
+ }
+ if !s.failed {
+ s.failed = true
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func filesSeedPendingConstruction(t *testing.T, store release.Store, resources ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor) {
+ if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
+ t.Fatal(err)
+ }
+ for _, path := range []string{"/run/carlos", "/run/carlos/hostguard"} {
+ if err := os.Mkdir(path, 0755); err == nil {
+ t.Cleanup(func() { os.RemoveAll(path) })
+ } else if !os.IsExist(err) {
+ t.Fatal(err)
+ }
+ }
+ emptyUnits := t.TempDir()
+ if err := unix.Mount(emptyUnits, "/etc/systemd/system", "", unix.MS_BIND, ""); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { unix.Unmount("/etc/systemd/system", 0) })
+ f := newRecoveryFixture(t)
+ obs := f.read()
+ obs.Claim.Op.Exec = actor.Box
+ obs.Claim.Op.Participants = []string{actor.Box}
+ obs.Claim.Touched.Boxes = []string{actor.Box}
+ claimBody, err := json.Marshal(obs.Claim)
+ if err != nil {
+ t.Fatal(err)
+ }
+ claimKey, err := hostclaim.Key(obs.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.claims.Put(t.Context(), claimKey, claimBody, obs.ETag); err != nil {
+ t.Fatal(err)
+ }
+ h := obs.Claim.Holder
+ plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: obs.Host, Account: h.Account, App: h.App, Channel: "stable", CreatedAt: f.now})
+ keys, err := f.claims.List(t.Context(), "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, key := range keys {
+ raw, _, err := f.claims.Get(t.Context(), key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = store.Put(t.Context(), key, raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ obs, err = hostclaim.Read(t.Context(), store, obs.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, etag, err := store.Get(t.Context(), h.Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resources.DataDir = "/data"
+ resources.UnitsDir = "/etc/systemd/system"
+ reg, err := openServiceRegistry("/data/registry.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ journal, err := openClaimRootJournal()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ journals, err := hostbox.NewJournalService(journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ state, err := openClaimRootAuthority()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer state.Close()
+ boundary := &r9PendingBoundary{Store: state}
+ authority := hostguard.NewRootAuthority(boundary)
+ roots := map[string]string{"data": "/data", "opt": resources.OptDir, "units": "/etc/systemd/system"}
+ guard := hostguard.Guard{Root: authority, DataDir: "/data", Claims: hostguard.StoreReader{Store: store}}
+ census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: reg, Launches: journal, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Roots: roots, SharedPaths: claimGlobalSharedPaths(resources)}
+ root := ClaimRootHTTP{Journals: journals, Launches: journal, Roots: roots, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Inventory: journals.Inventory, Census: census, Box: actor.Box, Enrollment: actor.Enrollment}, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard, Evidence: store}}}
+ _, client := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) {
+ out, err := root.commitRootInventory(ctx, q)
+ t.Logf("pending commit: %v", err)
+ return out, err
+ })
+ _, err = client.WriteExpected(t.Context(), hostbox.StopInventory{Complete: true, Host: obs.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1"}, hostbox.InventoryExpectation{RecordKey: h.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: obs.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: actor.Box, Enrollment: actor.Enrollment, ClaimETag: obs.ETag, Op: obs.Claim.Op.ID, Phase: obs.Claim.Op.Phase}})
+ if err == nil || !boundary.failed {
+ t.Fatal("pending boundary not reached", err)
+ }
+ if pending, err := authority.PendingInstallations(); err != nil || len(pending) != 1 {
+ t.Fatal("pending intent not durable", len(pending), err)
+ }
+ if err := verifyRootMigrationStartup(t.Context(), rootFloorIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment}, journals.Inventory); err == nil {
+ t.Fatal("pending state admitted new work")
+ }
+}
+
+func TestPlan14GuardRecoveryLegacyPolicyRootRunner(t *testing.T) {
+ if os.Getenv("PLAN14_R9_GUARD_CHILD") == "1" {
+ RunSystemGuardExec([]string{"recover"})
+ t.Fatal("guard returned")
+ }
+ dir := generationRootRunnerDir(t)
+ t.Setenv("CARLOS_OPT_DIR", dir)
+ t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy")
+ pub, key, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
+ remote := filepath.Join(dir, "remote")
+ t.Setenv("CARLOS_DEPLOYMENT_DIR", remote)
+ floor, _ := json.Marshal(hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"})
+ auth, err := release.SignRootEnvelope(release.RootSigner(key), hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "deploy", Key: hostfloor.Key, SHA256: hostfloor.Digest(floor)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "deploy", Mode: "enforced", Floor: floor, Authorization: auth})
+ if err := os.MkdirAll(systemGuardDir(dir), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(systemGuardDir(dir), "policy.json"), policy, 0644); err != nil {
+ t.Fatal(err)
+ }
+ store := release.NewDirStore(remote)
+ other, _ := json.Marshal(hostfloor.Floor{Min: 2, MinRelease: "v2.0.0"})
+ otherAuth, err := release.SignRootEnvelope(release.RootSigner(key), hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: "deploy", Key: hostfloor.Key, SHA256: hostfloor.Digest(other)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Put(t.Context(), hostfloor.Key, other, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := store.Put(t.Context(), hostfloor.AuthKey, otherAuth, ""); err != nil {
+ t.Fatal(err)
+ }
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ key := strings.TrimPrefix(r.URL.Path, "/deploy/")
+ raw, _, err := store.Get(r.Context(), key)
+ if err != nil {
+ http.Error(w, "missing", http.StatusNotFound)
+ return
+ }
+ w.Write(raw)
+ }))
+ defer server.Close()
+ for k, v := range map[string]string{"CARLOS_S3_ENDPOINT": server.URL, "AWS_REGION": "eu-west-1", "AWS_DEFAULT_REGION": "eu-west-1", "AWS_ACCESS_KEY_ID": "test", "AWS_SECRET_ACCESS_KEY": "test", "AWS_SESSION_TOKEN": "", "AWS_EC2_METADATA_DISABLED": "true"} {
+ t.Setenv(k, v)
+ }
+ binary, _ := os.ReadFile("/usr/bin/echo")
+ if err := os.WriteFile(systemBinaryPath(dir), binary, 0755); err != nil {
+ t.Fatal(err)
+ }
+ for _, flag := range []string{"", "1"} {
+ cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GuardRecoveryLegacyPolicyRootRunner$")
+ cmd.Env = append(os.Environ(), "PLAN14_R9_GUARD_CHILD=1", "CARLOS_HOST_CLAIM="+flag)
+ out, err := cmd.CombinedOutput()
+ if err == nil || !strings.Contains(string(out), hostfloor.ErrFloor.Error()) {
+ t.Fatal("legacy recovery ignored reachable policy mismatch", string(out), err)
+ }
+ }
+}
+
+const r9FloorRebootDir = "/var/lib/carlos-rootrunner-r9-floor"
+
+func TestRootRebootFloorPrevAuthorizationPhase1(t *testing.T) {
+ if err := os.Mkdir(r9FloorRebootDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ pub, key, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub))
+ id := rootFloorIdentity{"deploy", "box", "en"}
+ f, err := openRootMovesFloors(r9FloorRebootDir, id, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.close()
+ bound := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
+ if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &bound, Source: "accepted history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
+ t.Fatal(err)
+ }
+ for _, p := range []string{systemBinaryPath(r9FloorRebootDir), systemPrevPath(r9FloorRebootDir)} {
+ rootFloorSignRunnerBytes(t, r9FloorRebootDir, p, "v2.0.0", []byte("#!/bin/sh\nprintf recovered\n"), key)
+ }
+ j := rootFloorJudge{floors: f, current: systemBinaryPath(r9FloorRebootDir), prev: systemPrevPath(r9FloorRebootDir), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(r9FloorRebootDir, p) }, idle: func(context.Context) error { return nil }}
+ if err := j.judge(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ rootFloorSignRunnerBytes(t, r9FloorRebootDir, j.current, "v3.0.0", []byte("#!/bin/sh\nexit 99\n"), key)
+ if err := os.MkdirAll(systemRootDir(r9FloorRebootDir), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeFileDurable(filepath.Join(systemRootDir(r9FloorRebootDir), "journal.json"), []byte("{}"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := writeFileDurable(filepath.Join(r9FloorRebootDir, "boot"), boot, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := writeFileDurable(filepath.Join(r9FloorRebootDir, "pub"), []byte(base64.StdEncoding.EncodeToString(pub)), 0600); err != nil {
+ t.Fatal(err)
+ }
+}
+func TestRootRebootFloorPrevAuthorizationPhase2(t *testing.T) {
+ defer os.RemoveAll(r9FloorRebootDir)
+ before, err := os.ReadFile(filepath.Join(r9FloorRebootDir, "boot"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ now, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil || bytes.Equal(before, now) {
+ t.Fatal("actual reboot required", err)
+ }
+ pub, err := os.ReadFile(filepath.Join(r9FloorRebootDir, "pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", string(pub))
+ f, err := openRootMovesFloors(r9FloorRebootDir, rootFloorIdentity{"deploy", "box", "en"}, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.close()
+ raw, err := f.public.Read("prev.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var verdict rootFloorVerdict
+ if err := json.Unmarshal(raw, &verdict); err != nil {
+ t.Fatal(err)
+ }
+ prev, err := readRootFloorBinary(r9FloorRebootDir, systemPrevPath(r9FloorRebootDir))
+ if err != nil {
+ t.Fatal(err)
+ }
+ verdict.Prev.File.Device = prev.File.Device + 1
+ raw, _ = json.Marshal(verdict)
+ if err := f.public.Write("prev.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ j := rootFloorJudge{floors: f, current: systemBinaryPath(r9FloorRebootDir), prev: systemPrevPath(r9FloorRebootDir), binary: func(p string) (rootFloorBinary, error) { return readRootFloorBinary(r9FloorRebootDir, p) }}
+ journal, err := rootFloorJournalPresent(r9FloorRebootDir)
+ if err != nil || !journal {
+ t.Fatal("interrupted update missing", err)
+ }
+ selected, err := rootFloorBootCandidate(t.Context(), j, journal, func(context.Context, string) error { return nil }, func(context.Context) error { return nil })
+ if err != nil || selected != j.prev {
+ t.Fatal("accepted signed prev rejected after reboot", selected, err)
+ }
+ out, err := exec.Command(selected, "system", "apply", "--recover").CombinedOutput()
+ if err != nil || string(out) != "recovered" {
+ t.Fatal(string(out), err)
+ }
+ if err := os.WriteFile(j.prev, []byte("substitution"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := rootFloorPrevVerdict(j); err == nil {
+ t.Fatal("substituted prev accepted")
+ }
+}
diff --git a/internal/host/plan14_root_migration_test.go b/internal/host/plan14_root_migration_test.go
index ebdda62e9..b750524bd 100644
--- a/internal/host/plan14_root_migration_test.go
+++ b/internal/host/plan14_root_migration_test.go
@@ -278,6 +278,10 @@ func TestPlan14RootMigrationStartupVerification(t *testing.T) {
func fixtureWithoutRootMigrationCensus(t *testing.T) {
t.Helper()
+ oldOpen, oldShapes := openClaimActivationState, productionClaimUnitShapes
+ t.Cleanup(func() { openClaimActivationState, productionClaimUnitShapes = oldOpen, oldShapes })
+ openClaimActivationState = func(string, bool, bool) (rootstate.Store, error) { return nil, os.ErrNotExist }
+ productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
original := openRootMigrationParticipation
openRootMigrationParticipation = func() (rootparticipation.Ledger, bool, error) { return rootparticipation.Ledger{}, false, nil }
t.Cleanup(func() { openRootMigrationParticipation = original })
diff --git a/internal/host/proofroot.go b/internal/host/proofroot.go
index 2640fe418..4e5350081 100644
--- a/internal/host/proofroot.go
+++ b/internal/host/proofroot.go
@@ -174,7 +174,7 @@ func (r proofRoot) stillLive(rt registry.Route, o liveObservation) bool {
// if it qualifies); every other route reuses recordLive's observation.
func (r proofRoot) mintAndLog(ctx context.Context, obs proofObservations) {
pol, perr := obs.pol, obs.perr
- mintAllowed := false
+ mintAllowed := r.refusals == nil && r.recordProgram == nil
if perr == nil && pol != nil && pol.Prove != nil {
var err error
if r.mintCensus != nil {
@@ -343,3 +343,29 @@ func readProvingKey(path string) (ed25519.PrivateKey, error) {
}
return release.ParseSigningKey(string(b), path)
}
+
+func configureRootProof(pr *proofRoot, checks rootRestartChecks, open func() (*release.RootHistoryPass, error)) (*release.RootHistoryPass, error) {
+ legacy, err := checks.allowLegacy()
+ if err == nil && legacy {
+ return nil, nil
+ }
+ pr.mintCensus = func() (bool, error) {
+ legacy, err := checks.allowLegacy()
+ return !legacy && err == nil, err
+ }
+ var history *release.RootHistoryPass
+ pr.refusals = func(b release.HistoryBinding) ([]release.Refusal, error) {
+ if history == nil {
+ return nil, os.ErrPermission
+ }
+ return checks.mintRefusals(history.History, b)
+ }
+ if err != nil {
+ return nil, err
+ }
+ history, err = open()
+ if err == nil && history != nil {
+ pr.recordProgram = func(b liveBuild) error { return recordRootProgram(history, b) }
+ }
+ return history, err
+}
diff --git a/internal/host/proofroot_test.go b/internal/host/proofroot_test.go
index da239ae84..eb5ec6749 100644
--- a/internal/host/proofroot_test.go
+++ b/internal/host/proofroot_test.go
@@ -458,3 +458,29 @@ func sha256Hex(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:])
}
+
+func TestPlan14NoCensusProofKeepsMain(t *testing.T) {
+ for _, flag := range []string{"", "1"} {
+ t.Run("flag="+flag, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", flag)
+ e := newProofRootEnv(t, provePolicy)
+ e.root.mintCensus = nil
+ before, _ := os.ReadDir(e.bake)
+ history, err := configureRootProof(&e.root, rootRestartChecks{legacy: func() (bool, error) { return true, nil }}, func() (*release.RootHistoryPass, error) {
+ t.Fatal("no-census history creation")
+ return nil, os.ErrPermission
+ })
+ if err != nil || history != nil || e.root.recordProgram != nil || e.root.refusals != nil || e.root.mintCensus != nil {
+ t.Fatal("new root policy on legacy box", err)
+ }
+ after, _ := os.ReadDir(e.bake)
+ if len(before) != len(after) {
+ t.Fatal("configuration wrote state")
+ }
+ e.root.run(t.Context())
+ if e.proof(t) == nil {
+ t.Fatal("legacy proving box stopped minting", e.logs)
+ }
+ })
+ }
+}
diff --git a/internal/host/roothistory_creation.go b/internal/host/roothistory_creation.go
index 18b9ac997..c2515a0b7 100644
--- a/internal/host/roothistory_creation.go
+++ b/internal/host/roothistory_creation.go
@@ -26,9 +26,7 @@ func productionCreationHistory(ctx context.Context, effect func(*release.RootHis
if legacy {
return os.ErrPermission
}
- return withRootReleaseStore(ctx, func() (rootstate.Store, error) {
- return rootstate.Open(rootReleasePrivate, true, false)
- }, func() error {
+ return participationRelease.With(ctx, func(ctx context.Context) error {
s, err := rootstate.Open(release.RootHistoryDir, true, false)
if err != nil {
return err
diff --git a/internal/host/rootinventory_startup.go b/internal/host/rootinventory_startup.go
new file mode 100644
index 000000000..6c3dd851e
--- /dev/null
+++ b/internal/host/rootinventory_startup.go
@@ -0,0 +1,61 @@
+package host
+
+import (
+ "context"
+ "strconv"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func recoverRootInventoryStartup(ctx context.Context, store release.Store, r ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor, journals *hostbox.JournalService, launches *hostbox.LaunchJournal) error {
+ l, required, err := openRootMigrationParticipation()
+ if l.State != nil {
+ defer l.State.Close()
+ }
+ if err != nil || !required {
+ return err
+ }
+ c, err := l.RecoveryCensus()
+ if err != nil {
+ return err
+ }
+ state, err := rootstate.Open(rootMigrationDir, false, false)
+ if err != nil {
+ return err
+ }
+ defer state.Close()
+ if _, err := readRootMigrationState(state, l.State, rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: c.Migration}); err != nil {
+ return err
+ }
+ fences, err := openClaimRootAuthority()
+ if err != nil {
+ return err
+ }
+ defer fences.Close()
+ authority := hostguard.NewRootAuthority(fences)
+ pending, err := authority.PendingInstallations()
+ if err != nil || len(pending) == 0 {
+ return err
+ }
+ roots := map[string]string{"data": r.DataDir, "opt": r.OptDir, "units": r.UnitsDir}
+ guard := hostguard.Guard{Root: authority, DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}
+ census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: r.Registry, Launches: launches, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: strconv.FormatUint(actor.Revision, 10), Roots: roots, SharedPaths: claimGlobalSharedPaths(r)}
+ h := ClaimRootHTTP{history: productionCreationHistory, inventoryIdentity: rootstate.ObjectIdentity, Journals: journals, Launches: launches, Roots: roots,
+ InventoryActor: func(ctx context.Context) (hostbox.Actor, error) { return owners.actorForConstruction(ctx, r.Mode) },
+ Reconcile: hostbox.ReconcileAdapter{Registry: r.Registry, Inventory: journals.Inventory, Census: census, Box: actor.Box, Enrollment: actor.Enrollment},
+ Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard, Evidence: store}}}
+ if err := h.recoverRootInventory(ctx); err != nil {
+ return err
+ }
+ pending, err = authority.PendingInstallations()
+ if err != nil {
+ return err
+ }
+ if len(pending) != 0 {
+ return errRootMigrationBusy
+ }
+ return nil
+}
diff --git a/internal/host/rootmigration.go b/internal/host/rootmigration.go
index 7822d2743..99d944ebd 100644
--- a/internal/host/rootmigration.go
+++ b/internal/host/rootmigration.go
@@ -59,6 +59,13 @@ func (s rootMigrationSnapshot) preflight(id rootMigrationIdentity) error {
return errRootMigrationBusy
}
for _, rec := range s.Records {
+ if !claimNamedUnit4aComplete && rec.Unit != "" {
+ for _, item := range s.Items {
+ if item.Installation.Host == rec.Host {
+ return errRootMigrationBusy
+ }
+ }
+ }
if rec.Seed.Unfinished() {
return errRootMigrationBusy
}
diff --git a/internal/host/rootmigration_bootstrap_root_linux_test.go b/internal/host/rootmigration_bootstrap_root_linux_test.go
index 66b9ed619..733d5d84d 100644
--- a/internal/host/rootmigration_bootstrap_root_linux_test.go
+++ b/internal/host/rootmigration_bootstrap_root_linux_test.go
@@ -77,7 +77,7 @@ func testRootMigrationBootstrap(t *testing.T, historic, fresh bool) {
if fresh {
name = "TestPlan14RootMigrationFreshStopRootRunner"
}
- if t.Name() == "TestRootRebootPlan14MigrationInstalledPhase1" {
+ if t.Name() == "TestRootRebootPlan14MigrationInstalledPhase1" || t.Name() == "TestPlan14InitialMigrationExclusionRootRunner" {
name = t.Name()
}
cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^"+name+"$", "-test.v")
@@ -167,6 +167,9 @@ func testRootMigrationBootstrap(t *testing.T, historic, fresh bool) {
t.Fatal(err)
}
+ if t.Name() == "TestPlan14InitialMigrationExclusionRootRunner" {
+ plan14InitialMigrationExclusion(t, store, r, owners.ClaimOwnerBindings, actor, service, journal)
+ }
observedAfter := time.Now().UTC()
oldOwner := claimQuiesceOwner
claimQuiesceOwner = func() (int, int, error) { return 65534, 65534, nil }
@@ -338,7 +341,7 @@ func testRootMigrationBootstrap(t *testing.T, historic, fresh bool) {
if err := g.With(t.Context(), target.Host, captured, allow, launch); err != nil || effects != 1 {
t.Fatal("successor writer", err, effects)
}
- if t.Name() == "TestRootRebootPlan14MigrationInstalledPhase1" {
+ if t.Name() == "TestRootRebootPlan14MigrationInstalledPhase1" || t.Name() == "TestPlan14InitialMigrationExclusionRootRunner" {
if err := journal.Close(); err != nil {
t.Fatal(err)
}
diff --git a/internal/host/rootmigration_production_linux.go b/internal/host/rootmigration_production_linux.go
index 724b82805..39152adcb 100644
--- a/internal/host/rootmigration_production_linux.go
+++ b/internal/host/rootmigration_production_linux.go
@@ -6,8 +6,10 @@ import (
"encoding/hex"
"encoding/json"
"errors"
+ "golang.org/x/sys/unix"
"os"
"path/filepath"
+ "strconv"
"strings"
"time"
@@ -26,7 +28,13 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim
if err := claimActivationPreflight(r.Registry); err != nil {
return err
}
- return withClaimRootRelease(ctx, true, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) }, func() error {
+ return withRootReleaseStore(ctx, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) }, func() error {
+ if err := claimActivationVerified(ctx, store, r); err != nil {
+ return err
+ }
+ if err := quiesceLegacyRootMutators(ctx); err != nil {
+ return err
+ }
state, err := rootstate.Open(rootMigrationDir, true, false)
if err != nil {
return err
@@ -233,3 +241,41 @@ func rootMigrationLegacySuspects(ctx context.Context, dd string) ([]string, erro
}
return out, nil
}
+
+func quiesceLegacyRootMutators(ctx context.Context) error {
+ entries, err := os.ReadDir("/proc")
+ if err != nil {
+ return err
+ }
+ for _, entry := range entries {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ pid, err := strconv.Atoi(entry.Name())
+ if err != nil || pid == os.Getpid() {
+ continue
+ }
+ path := "/proc/" + entry.Name()
+ var st unix.Stat_t
+ if err := unix.Stat(path, &st); errors.Is(err, unix.ENOENT) {
+ continue
+ } else if err != nil {
+ return err
+ }
+ if st.Uid != 0 {
+ continue
+ }
+ raw, err := os.ReadFile(path + "/cmdline")
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ if err != nil {
+ return err
+ }
+ args := strings.Split(string(raw), "\x00")
+ if len(args) > 1 && (args[1] == "adopt" || args[1] == "system" && len(args) > 2 && (args[2] == "apply" || args[2] == "guard-arm")) {
+ return errRootMigrationBusy
+ }
+ }
+ return nil
+}
diff --git a/internal/host/rootmigration_startup.go b/internal/host/rootmigration_startup.go
index fce08ca96..63f4514be 100644
--- a/internal/host/rootmigration_startup.go
+++ b/internal/host/rootmigration_startup.go
@@ -47,22 +47,29 @@ func verifyRootMigrationRetained(ctx context.Context, root *hostguard.RootAuthor
return nil
}
func verifyRootMigrationState(ctx context.Context, state, execution rootstate.Store, id rootMigrationIdentity, root *hostguard.RootAuthority) error {
- c, err := (rootparticipation.Ledger{State: execution}).Census()
+ r, err := readRootMigrationState(state, execution, id)
if err != nil {
return err
}
+ return verifyRootMigrationRetained(ctx, root, r.Snapshot)
+}
+func readRootMigrationState(state, execution rootstate.Store, id rootMigrationIdentity) (rootMigrationRecord, error) {
+ var r rootMigrationRecord
+ c, err := (rootparticipation.Ledger{State: execution}).RecoveryCensus()
+ if err != nil {
+ return r, err
+ }
if c.Box != id.Box || c.Enrollment != id.Enrollment || c.Migration != id.Generation || !c.Complete || c.NeverEnrolled {
- return errRootMigrationBusy
+ return r, errRootMigrationBusy
}
raw, err := state.Read("migration.json")
if err != nil {
- return err
+ return r, err
}
- var r rootMigrationRecord
if rootparticipation.Decode(raw, &r) != nil || r.Schema != 1 || r.Identity != id || r.Phase != "complete" || r.Snapshot.preflight(id) != nil {
- return errRootMigrationBusy
+ return r, errRootMigrationBusy
}
- return verifyRootMigrationRetained(ctx, root, r.Snapshot)
+ return r, nil
}
func verifyRootMigrationStartup(ctx context.Context, id rootFloorIdentity, inventory *hostbox.StopInventoryDB) error {
l, required, err := openRootMigrationParticipation()
@@ -116,10 +123,13 @@ func verifyRootMigrationFloors(opt string, id rootFloorIdentity) error {
if err != nil {
return err
}
- if !b.supports(bound) {
+ if !b.supports(nil) {
return errRootFloorHistory
}
- if err := rootSeedsAllowsBinary(opt, id, path); err != nil {
+ if !b.supports(bound) {
+ rootFloorAlert()
+ }
+ if _, err := readRootSeedsBound(opt, id); err != nil {
return err
}
}
diff --git a/internal/host/rootmovesfloor_boot.go b/internal/host/rootmovesfloor_boot.go
index 66b34a0b1..7d105dbc6 100644
--- a/internal/host/rootmovesfloor_boot.go
+++ b/internal/host/rootmovesfloor_boot.go
@@ -44,7 +44,7 @@ func rootFloorPrevVerdict(j rootFloorJudge) error {
if err != nil {
return err
}
- if binary != v.Prev || !j.permits(binary, p.Floor) {
+ if !sameRootFloorImage(binary, v.Prev) || !j.permits(binary, p.Floor) {
return errRootFloorHistory
}
return nil
@@ -190,3 +190,10 @@ func rootFloorJournalPresent(opt string) (bool, error) {
}
return true, f.Close()
}
+
+func sameRootFloorImage(a, b rootFloorBinary) bool {
+ // Each observation reopens a root-owned image through checked ancestry
+ // and verifies its bytes against the signed identity. Device numbers and
+ // inode numbers are not authorization across boots or signed repairs.
+ return a.Digest != "" && a.Digest == b.Digest && a.Version == b.Version && a.Protocol == b.Protocol && a.SeedsProtocol == b.SeedsProtocol
+}
diff --git a/internal/host/rootmovesfloor_boot_test.go b/internal/host/rootmovesfloor_boot_test.go
index 61f10bd81..cc631c961 100644
--- a/internal/host/rootmovesfloor_boot_test.go
+++ b/internal/host/rootmovesfloor_boot_test.go
@@ -76,3 +76,32 @@ func TestPlan14RootFloorBootDecision(t *testing.T) {
})
}
}
+
+func TestPlan14PrevRebootSafeVerdict(t *testing.T) {
+ f := rootMovesFloors{private: &rollbackMemory{files: map[string][]byte{}}, public: &rollbackMemory{files: map[string][]byte{}}, identity: rootFloorIdentity{"d", "b", "e"}}
+ bound := moves.Floor{Min: 1, MinRelease: "v2.0.0"}
+ if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: f.identity, Floor: &bound, Source: "accepted", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil {
+ t.Fatal(err)
+ }
+ prev := rootFloorBinary{Version: "v2.0.0", Protocol: 1, Digest: floorTestDigest}
+ broken := false
+ j := rootFloorJudge{floors: f, current: "current", prev: "prev", binary: func(path string) (rootFloorBinary, error) {
+ if path == "current" && broken {
+ return rootFloorBinary{}, os.ErrPermission
+ }
+ return prev, nil
+ }, idle: func(context.Context) error { return nil }}
+ if err := j.judge(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ prev.File.Device++
+ broken = true
+ got, err := rootFloorBootCandidate(t.Context(), j, true, func(context.Context, string) error { return nil }, func(context.Context) error { return nil })
+ if err != nil || got != "prev" {
+ t.Fatal("interrupted update lost unchanged previous image after device renumber", got, err)
+ }
+ prev.Digest = "substituted"
+ if err := rootFloorPrevVerdict(j); err == nil {
+ t.Fatal("substituted bytes accepted")
+ }
+}
diff --git a/internal/host/rootreleaselock.go b/internal/host/rootreleaselock.go
index 32a072037..39ff11cc3 100644
--- a/internal/host/rootreleaselock.go
+++ b/internal/host/rootreleaselock.go
@@ -2,6 +2,7 @@ package host
import (
"context"
+ "errors"
"github.com/carlosframework/platform/internal/rootparticipation"
"os"
@@ -36,7 +37,7 @@ func withClaimReleaseCensus(ctx context.Context, census func() (rootparticipatio
return err
}
if !required {
- return effect()
+ return withExistingRootRelease(ctx, effect)
}
return withRootReleaseStore(ctx, open, effect)
}
@@ -54,3 +55,20 @@ type adoptRootSource struct {
OpenRelease func() (rootstate.Store, error)
Census func() (rootparticipation.Ledger, bool, error)
}
+
+func withExistingRootRelease(ctx context.Context, effect func() error) error {
+ if _, err := os.Lstat(rootReleasePrivate); errors.Is(err, os.ErrNotExist) {
+ return effect()
+ } else if err != nil {
+ return err
+ }
+ s, err := rootstate.Open(rootReleasePrivate, false, false)
+ if errors.Is(err, os.ErrNotExist) {
+ return effect()
+ }
+ if err != nil {
+ return err
+ }
+ defer s.Close()
+ return rootstate.WithRelease(ctx, s, effect)
+}
diff --git a/internal/host/systemapply.go b/internal/host/systemapply.go
index f3f78fae1..2865e0a3c 100644
--- a/internal/host/systemapply.go
+++ b/internal/host/systemapply.go
@@ -135,7 +135,7 @@ func (a *systemApplier) runWithRelease(ctx context.Context, recover bool) error
}
return a.participationDisk().WithParticipation(ctx, func(ctx context.Context, _ rootparticipation.Ledger) error {
return a.Run(ctx, recover)
- }, func() error { return a.Run(ctx, recover) })
+ }, func() error { return withExistingRootRelease(ctx, func() error { return a.Run(ctx, recover) }) })
}
// openSystemStore is the production openStore: the deploy store and the
@@ -1055,6 +1055,9 @@ func (a *systemApplier) activationGuard(ctx context.Context, target, running str
// floorRefusal is the floor half of the rule.
func floorRefusal(version string, floor *moves.Floor) error {
+ if floor == nil {
+ return nil
+ }
if version == "" {
return refuse("moves protocol floor: carlos.prev records no version, so it is below %s", floor.MinRelease)
}
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv
index 4c1dc66e7..f7c50d92e 100644
--- a/internal/provision/testdata/plan14-root-reads-p3.tsv
+++ b/internal/provision/testdata/plan14-root-reads-p3.tsv
@@ -2,12 +2,12 @@
# Earlier overlays and frozen legacy fingerprints are unchanged.
internal/console/boxselect.go Server.selectMoveBoxes authority F,J4 rev3=PROPOSED-p3;identity=carlos console serve, console.service uid carlos-console;basis=applies accepted/generation refusal only to advertised root-floor participants; legacy selection retains main exactly d51a6a29a190dd36e896567adf4e0e0e1c8c9a08ff909cfbedd7732c2321ae78
internal/console/seedfloor.go Server.seedRegionReady authority F,J4 rev3=PROPOSED-p3;identity=carlos console serve uid carlos-console;basis=requires accepted seeds generation only from root-floor participants; mixed fleet nonparticipants retain exact legacy readiness d116ff2f72a1386a3a4120c69fea671cd3d43af264a20a1b8e98b5c61f11efb2
-internal/host/claimconstruction.go OpenProductionClaimAssembly authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=verifies complete retained migration and typed inventories before constructing claim authority on a census box; no census preserves prior path;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=checks the named-unit cohort before owner construction or state creation; retained census and migration checks remain mandatory file:Read@10210,file:Read@4452,file:ReadFile@3996,registry:Host@10237,whole:Route@10325,whole:Route@11074 9e6114edd3dd8aeaad99b675e5e908d480a65a32c593105c1ed2b5bb44789d75
+internal/host/claimconstruction.go OpenProductionClaimAssembly authority G,J4 rev3=PROPOSED-p3;run=13;identity=agent uid carlos; adopt and instance-control uid 0;basis=checks registry and independently read canonical records before construction; root also checks retained unit state and settles retained pending inventory before full launch-inventory verification file:Read@10442,file:Read@4684,file:ReadFile@4228,registry:Host@10469,whole:Route@10557,whole:Route@11306 190aa844e443768c8dc4557289e89e5d8106a638a8dec759bbe0b313802a4c3c
internal/host/claimjournal_migration.go RunSystemJournalMigrate authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=explicit offline root operator entry requests full G2 bootstrap after verified claim owner setup 4f204d8a97620bfb22430a94e236d6c5b14a5265b35f68e21fb2ad34903ae0f3
-internal/host/claimjournal_migration.go migrateClaimJournalsWith authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=opens verified actor and root journal then selects full offline census or unchanged legacy journal import;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures abaa6a3714e7de7b95f6249f5c8d3a6fb49fae2777e5456f9811d97bc4ad1d9a
+internal/host/claimjournal_migration.go migrateClaimJournalsWith authority G,J4 rev3=PROPOSED-p3;run=13;identity=offline migration uid 0;basis=checks canonical cohort eligibility before opening migration owners and journals b66afddd13e0f346ddecb481e88e58d3ad4a40b703f9f8199bdb6641fda6f54e
internal/host/guardarm.go RunSystemGuardArm authority F,J4 rev3=PROPOSED-p3;identity=carlos system guard-arm, offline CLI uid 0;basis=root guard-arm checks retained census floor before installing guard; no census retains main file:OpenFile@565,file:ReadFile@1142,file:ReadFile@1756,file:ReadFile@5797,file:ReadFile@865,file:Stat@3448 3eb11b09adc3b122f555513cc35aac8124f89149e72db995351cde384b2cf809
-internal/host/guardexec.go RunSystemGuardExec authority F,J4 rev3=PROPOSED-p3;identity=carlos system guard-exec apply/recover root units uid 0; edge unit uid carlos; console unit uid carlos-console;basis=root apply/recover selects signed image from local bound; non-root edge bypasses private floor entirely file:Exec@1960,file:Exec@2113,file:Stat@976 6f47b37437a4072284e0965d13a60c9cb9a7a9b048be140238dc1e6b57b44675
-internal/host/guardtransaction.go systemApplier.guardRun authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover, root systemd units or offline root CLI, uid 0;basis=verifies signed target digest and local retained bounds before executing a version or bootstrap request c0a0ea8fe6a524d95b3a579ecf296c8075e435478c6daae2b0fa06518e88314f
+internal/host/guardexec.go RunSystemGuardExec authority F,J4 rev3=PROPOSED-p3;run=13;identity=root apply/recover uid 0; edge uid carlos; console service identity;basis=keeps main remote policy checks for no-census recovery; only census recovery is network-free; edge never opens root state file:Exec@2093,file:Exec@2246,file:Stat@1109 7b0dc632eeb11e793d3262c010f6fe8deab057be431c0783e696529afd643bb4
+internal/host/guardtransaction.go systemApplier.guardRun authority F,J4 rev3=PROPOSED-p3;run=13;identity=root system update/recovery uid 0;basis=checks signed image and bound, permits certified current/guard helpers and reauthorized previous repair images below a raised bound, and refuses below-bound replacement candidates 12c0d6c2d9cdbe8ec95d8128aaff90aab410af511033a693a29eb655f2faa541
internal/host/movesfloor.go rootJudgeMovesPrevWithSource authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=adopt refreshes root floor only for census participants; legacy registry judgement otherwise unchanged file:Stat@289,file:Stat@490 fa942c1c4b8af2d2930be427e303f3b9838c0fce33f1b3f8e63e17093b638269
internal/host/movesfloor.go rootJudgePrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=normal adopt combines moves and seeds verdict errors; unpublished seeds remain inert; raised bounds never block recording signed prev 8b5ecf5a0f33ee58d603f308302bcc8e68678da6b1e6ee7ed03845c35747f8c6
internal/host/movesfloor.go rootJudgePrevWithSource authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, uid 0;basis=normal adopt path refreshes and judges both independent protocol floors only on census boxes 8d25e75b26a43b93f4c635b5d3a9d25bf41f60a02face307dc11034c706dd916
@@ -17,19 +17,19 @@ internal/host/rootmigration.go rootMigration.census authority G,J4 rev3=PROPOSED
internal/host/rootmigration.go rootMigration.run authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline uid 0;basis=offline guarded migration reobserves changed claims, retains union of session tuples, publishes only durable completion and verifies completed replay without rewriting census file:Read@508 5d110d3e531052516a19afc5877aa9e2e481d9bbf3305036421aad822699223e
internal/host/rootmigration.go rootMigration.write authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=serializes migration phase records through checked private store durability 25c2c65aaae5b4ca1761fe0e3c456fbcd29d8dc58e5bdd40f94bf2ffe855ca7d
internal/host/rootmigration.go rootMigrationSnapshot.fingerprint authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=binds exclusion and membership across observations/restart while allowing boot metadata and independently recovered payload revision 115b07f84d5aa210cd35d70e2ba4b53ff4deb0e4636a737a5dc144bf34a9473c
-internal/host/rootmigration.go rootMigrationSnapshot.preflight authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=refuses active moves, unfinished seeds, incomplete inventories or mismatched migration identity before effects 6bc599e1a9ad0502b08d3ab2d401783c335edfbbdeb2b77453e4d83a9bf5060c
+internal/host/rootmigration.go rootMigrationSnapshot.preflight authority G,J4 rev3=PROPOSED-p3;run=13;identity=offline root migration uid 0;basis=rechecks the independently pinned canonical records for local named installations before creating migration census 4ceedefec737b0e91de75d9aee0422e585ededb335dc3d26f8c97a1445394e2b
internal/host/rootmigration_census.go rootMigrationCollector.collect authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=signed collecting manifest and actor directory bound complete records/claims census; registry Seeds are refusal-only file:Exec@4684,file:Read@2868,file:Read@689 6cfe43917f24a6dd6c7e3eee503f84897c944ec0c251ddfc64c16884f5090438
internal/host/rootmigration_evidence.go readMigrationStop authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline operator uid 0;basis=revalidates enrolled immutable stop signature and exact launch inventory journal tuple; preserves original bytes and evidence time file:Read@592 ee9a331a4c022d045acff9eca5003f4df79f9f872ca132b1fa37b150d16338d9
internal/host/rootmigration_production_linux.go migrationExecutionEmpty authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=any retained execution entry other than census/locks refuses migration; conservative settled-record limitation remains c17efaf8e7191cf0cd36d0ee462793c78094e40aef46567a446e503aee494641
internal/host/rootmigration_production_linux.go publishMigrationReceipt authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=reconstructs immutable key/digest from original signed receipt bytes without changing evidence time db91dbe64b57fe10aa1635fb46938e2ccb585afb6aa138800507acac6a83c377
internal/host/rootmigration_production_linux.go randomMigrationID authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=cryptographic nonce identifies newly collected stop evidence, never authorizes a stop file:Read@64 c2779c80b5628eca7d484ffba0b289c1068317b1f474c06e1ee5b39c3f769196
internal/host/rootmigration_production_linux.go rootMigrationLegacySuspects authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=agent-writable hostguard filenames only identify unexplained hosts to refuse, never authorize inventory file:Readdirnames@277 6bfa73d903baa8a028abb007a7ec1da0066dbcda966e92e29b50a2b56fda10ab
-internal/host/rootmigration_production_other.go migrateRootClaimAuthority authority G,J4 rev3=PROPOSED-p3;identity=non-Linux offline journal-migrate CLI;basis=unsupported-platform stub returns ErrUnsupported without reading or granting migration authority 327e0adfe387eac7bc74b87258efc9831f5a35ee9c9c9e71640d4e6b54853aae
+internal/host/rootmigration_production_other.go migrateRootClaimAuthority authority G,J4 rev3=PROPOSED-p3;run=13;identity=offline migration uid 0;basis=takes mandatory release exclusion even before census exists, rechecks canonical cohort eligibility, and refuses active legacy root mutators before state/census transition 327e0adfe387eac7bc74b87258efc9831f5a35ee9c9c9e71640d4e6b54853aae
internal/host/rootmigration_startup.go var openRootMigrationParticipation authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=fixed production participation opener remains rootparticipation.OpenForUpdate; package tests substitute explicit no-census fixtures without accessing the machine store 53a17d908b433d98cdb817ab72543bd53788962625ccc9cfa0e48f829bbc1aa3
-internal/host/rootmigration_startup.go verifyRootMigrationFloors authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=checks installed and guard signed binaries against both locally retained protocol bounds without remote access 257eac6eb0de322ca5ad251f98579f81c4fcad58aa51f586edae11b9e14004c9
+internal/host/rootmigration_startup.go verifyRootMigrationFloors authority G,F,J4 rev3=PROPOSED-p3;run=13;identity=root startup/migration uid 0;basis=requires certified current and guard repair executors and readable local bounds; a bound above installed executors emits the existing alert rather than blocking repair construction 7e558cfe3164a5a2d25a88dad074b85e1a23325ae98e777dac6c6c3543bc1228
internal/host/rootmigration_startup.go verifyRootMigrationRetained authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=requires each migrated installation to retain its old-session deny, exact stopped predecessor receipt and complete root inventory f2dc230494f4659e6d4bf71961d617854454bd8f713585a1cd4489414c288cb1
internal/host/rootmigration_startup.go verifyRootMigrationStartup authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=checks root-owned migration records and typed journal revisions before issuing startup grants; absent census leaves main path intact file:Open@337 b5149f8db369dbf474612ec6f6afe55c518f337f619479bdfeb648f518d4c5af
-internal/host/rootmigration_startup.go verifyRootMigrationState authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=binds complete root census and durable complete migration record to deployment box enrollment and migration generation file:Read@415 7d09ad99c73d534730d092d873ba558eb2a0d352b5f8f9a0d50b9530eba1dff2
+internal/host/rootmigration_startup.go verifyRootMigrationState authority G,F,J4 rev3=PROPOSED-p3;run=13;identity=root startup/recovery uid 0;basis=validates retained migration header then complete installations after pending recovery has settled 531c7d47066a7dfad787aed67f08ba6078091c7b9426d8f685872c4948c2d2ba
internal/host/rootmigration_stopped_linux.go verifyMigrationStopped authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline operator uid 0;basis=root checks real proc/cgroup mounts, closed writer journal and absence of cgroups and held inventory descriptors; all observations can only refuse recovery file:ReadFile@588 06b82196857608ace8bfaa8ce7108caad961868b530a0d9cb152326a1aaf6819
internal/host/rootmovesfloor.go openRootMovesFloors authority F,J4 rev3=PROPOSED-p3;identity=root adopt refresh, root system update/recovery and root guard floor readers, uid 0;basis=selects fixed moves namespace then delegates checked root store opening 4af0bda66779af3474e64fda54015763761d058feead1b8a2ebf0c2478a05a3d
internal/host/rootmovesfloor.go openRootProtocolFloors authority F,J4 rev3=PROPOSED-p3;identity=root adopt refresh, root system update/recovery and root guard floor readers, uid 0;basis=checked root ancestry and leaf ownership for separate moves or seeds public/private stores file:Open@170,file:Open@258 a3b589e05e8cdc1062162562006ef7f93cc3159f2f24a10a155c038ce7e3ca28
@@ -45,7 +45,7 @@ internal/host/rootmovesfloor_authorization.go rootMovesFloors.authorizedReconstr
internal/host/rootmovesfloor_boot.go rootFloorAllowsPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos.prev system floor-allows-prev --root, invoked by root guard selector, uid 0;basis=signed prev identity satisfies local moves and published seeds bounds, relaxed only when no certified build meets bound; caller checks participation separately 61dd5d5f0604f4f4d2bd4a396678a20f4b48e91b952882a94873b2f733ebe560
internal/host/rootmovesfloor_boot.go rootFloorBootCandidate authority F,J4 rev3=PROPOSED-p3;identity=carlos system guard-exec apply/recover, systemd root units uid 0;basis=selects signed prev under unknown history with no known refusal; always retains certified current executor even when bound exceeds every available build 8f78cd43bb047e128145147cd32d58b445e50aaacf4403946219b663eabac926
internal/host/rootmovesfloor_boot.go rootFloorJournalPresent authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=checked authority-file presence selects recovery without opening a deployment store b189d04dc579f4825f5668efd1f93f178acca738bb2ed886a14f3b5321d7755b
-internal/host/rootmovesfloor_boot.go rootFloorPrevVerdict authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=matches actual signed previous binary to identity and accepted generation before allowing its gate file:Read@436 c86e963acf816c48fd9b860db6d3e695c73e12db8705cc8c5977ccb5e876de10
+internal/host/rootmovesfloor_boot.go rootFloorPrevVerdict authority F,J4 rev3=PROPOSED-p3;run=13;identity=root boot/recovery selector uid 0;basis=reauthorizes previous bytes against signed root identities and accepted verdict; boot-local device and inode numbers cannot reject unchanged signed repair bytes file:Read@436 ad5acd71daa00ffbe34fe9b3cb7224b4d6ef1d9b21d1f3e6b2d1d213c88e2e4e
internal/host/rootmovesfloor_boot.go withRootFloorGuardCandidate authority F,J4 rev3=PROPOSED-p3;identity=carlos system guard-exec apply/recover, systemd root units uid 0;basis=release then previous-binary exclusion; participation check precedes moves and seeds locks; certified current remains executor fallback file:Open@218 74d00903cf028a74237153d25879f768a3bd8546cb107d85b4d11feb7f77e04c
internal/host/rootmovesfloor_identity.go rememberRootFloorSystemIdentity authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=re-fetches and verifies signed pointer equality before retaining its executable identity file:Open@543 b4e56a0637c28e5fb65d3a33b7dfd1fea8bed55acbf5231bf26e0011168141fe
internal/host/rootmovesfloor_refresh.go readRootFloorBinary authority F,J4 rev3=PROPOSED-p3;identity=root adopt refresh, root system update/recovery and root guard floor readers, uid 0;basis=verifies checked executable and signed system manifest; reads signed moves and seeds protocol numbers rather than version sidecar file:Open@684,file:Read@941,file:Stat@276 2a23dd9c46c6fa56476ecfa4061712f05c6f1a1a098295e14f395def2d24324a
@@ -102,7 +102,7 @@ internal/hostguard/root.go RootAuthority.write authority C,G,J4 rev3=PROPOSED-p3
internal/hostguard/rootmigration.go RootAuthority.RetainMigrationDeny authority C,G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=root retains old incarnation/stamp/session denial under stable per-host lock; grants no launch authority fb821d19b6ca6e1454d025013d27ef8a856a684262421d3ee76804723e875c37
internal/hostguard/rootmigration.go migrationAdmissionDenied authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=old migrated sessions remain denied; a newer same-installation session requires completed receipt for every retained migration predecessor c7312f5e0e67b2b69bbea1b8f432dd48ba06f0b4a26ebf349b6724769651631e
internal/hostguard/rootmigration.go migrationDenied authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=old sessions at/below retained captured stamp refuse root and agent admission 6ccf5c00e33d880aa50ea15d2bce22a3cce14455d5cc7f05197e42f789da8805
-internal/hostguard/rootmigration_inventory.go RootAuthority.MigrationInstallations authority C,G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0; also root claim startup uid 0;basis=read-only root installation enumeration includes incomplete records and never repairs before preflight ad855b461cb12e5499ecec74b210f6f41a758a3ed36c0eac0ef5931bc7fad6c2
+internal/hostguard/rootmigration_inventory.go RootAuthority.MigrationInstallations authority C,G,J4 rev3=PROPOSED-p3;run=13;identity=root migration/startup uid 0;basis=enumerates retained installations including zero-revision inventory; pending installation transactions are refused and must first settle through restricted startup recovery ad855b461cb12e5499ecec74b210f6f41a758a3ed36c0eac0ef5931bc7fad6c2
internal/hostguard/rootview.go FenceView.admit authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=read-only root view applies same predecessor-completion requirement as locked admission 5bcc6b69bb3e98a1bda6ace2b0b2a147f8c39b61601c609c30e3fb806231efae
internal/hostguard/rootview.go Guard.fenceLocked authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=projects root-only migration denials into agent admission snapshot without trusting local hostguard records 290295ed69e06548a41e101317098ae2d6aac10c41b260e120af55860686a96d
internal/hostguard/rootview.go RootAuthority.viewLocked authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=projects root migration denials together with canonical installation and existing fences 5db6824b38f6ad3e4072008ffa3ff9fab98db6a5d40ecba59ca30900e331a46f
@@ -141,7 +141,7 @@ internal/rootparticipation/transaction.go Ledger.lockTransaction authority D,F,J
internal/rootparticipation/transaction.go TransactionLedger authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover uid 0;basis=returns only an active root-minted held ledger; stale and cancelled scopes refuse 2046fbd7b7ec9455794dbaf614ea4cb50b9147929004434a172a67656fb7ff63
internal/rootparticipation/transaction.go type heldTransaction authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover uid 0;basis=binds transaction reuse to a root-opened ledger and live callback lifetime 4769022372ba86be2255c15a53c5d7acd88bc9e50652391e84284a436ae615c7
internal/rootparticipation/transaction.go type transactionKey authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover uid 0;basis=unexported key prevents callers from forging the held participation scope c309e812e20a85927d3cc93c8048d811c7187137797ad76646cc12f892d82a63
-internal/host/systemapply.go systemApplier.runWithRelease authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover or root participation caller uid 0;basis=root CLI carries E release exclusion through apply and recovery; nested guards reuse its token; absent census runs the exact legacy body 462d91eb211467885431108bc352b1913ea430fca5602434fc9fd7ef39713efa
+internal/host/systemapply.go systemApplier.runWithRelease authority D,F,J4 rev3=PROPOSED-p3;run=13;identity=root update/recovery uid 0;basis=census updates retain participation release exclusion; legacy updates honor explicit migration exclusion before acquiring apply lock ebdeb0eeb9e4300e3309961994efccab531fc9d2b10fb078f4c34e875d905106
internal/hostfloor/disk.go Disk.withParticipation authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover or root participation caller uid 0;basis=uses a live root-minted scoped ledger only for its original store; otherwise opens the ordinary census; absent census keeps legacy behavior 3bf4a54d1ab3621eee974a797ffa63a988369b9f245ceddde1cb99cc6386ec82
internal/rootparticipation/ledger.go Ledger.Idle authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover or root participation caller uid 0;basis=checks settled execution and local launch completeness under participation; nested activation uses only its already-held store e50d8f839d39f771ad4b6018f315cd7002a6e470424df0a46482906c31ef575e
internal/rootparticipation/ledger.go Ledger.RecoverAcquisition authority D,F,J4 rev3=PROPOSED-p3;identity=root system apply/recover or root participation caller uid 0;basis=retains acquisition-recovery checks and census commit under the same participation transaction; no new authority from a missing record d1f44274ba0a53aae7e39ce016da787748123665bda0cdf7a40339a4fc52e9fc
@@ -189,7 +189,7 @@ internal/rootparticipation/ledger.go Ledger.RecoverAcquisition authority D,F,J4
@removed internal/rootparticipation/production.go ActivationReady D,G census-ruling-J4
internal/historyreadback/readback.go RequestStatus diagnostic E1 rev3=PROPOSED-p3;identity=diagnostic request library, no production caller;basis=P3-R8 #2; signed readback request only; no reconstruction or retirement d89052b0cdd12c8c8dcbc353e9ae995005caeb88d8e4c70974cb9e6f4bfd46d7
internal/hostbox/migration_census_other.go CheckMigrationRoots hint G,J4 rev3=PROPOSED-p3;identity=non-Linux offline migration caller;basis=P3-R8 #17; unsupported platform refusal only 594ecda260fcf80baf5a2709c9cc5a04e91c655c8bd2f7ac307eef44216eb9c8
-internal/host/rootmigration_production_linux.go migrateRootClaimAuthority authority G,J4,E3 rev3=PROPOSED-p3;identity=offline migration root uid 0;basis=offline migration adopts pre-claim timers only after verified root stop evidence; resumes its exact saved intention under release and migration exclusion, rechecking kernel absence before completion;rev3=PROPOSED-p3;run=8;identity=offline journal-migrate uid 0;basis=initializes root failure history after a complete census; legacy bytes add only denies and never retry permission;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=refuses named-unit activation before opening release, migration, execution or fence state while 4a is incomplete file:Open@1616,file:Open@380,file:Open@462,file:Open@586,file:Open@6910,file:Read@2568,file:ReadFile@1418,whole:Route@2881 3c5dd2cdb8414b29b4996803e57557332bc53f44d04a6ff615b2a2c9638eca7b
+internal/host/rootmigration_production_linux.go migrateRootClaimAuthority authority G,J4,E3 rev3=PROPOSED-p3;run=13;identity=offline migration uid 0;basis=takes mandatory release exclusion even before census exists, rechecks canonical cohort eligibility, and refuses active legacy root mutators before state/census transition file:Open@1768,file:Open@374,file:Open@614,file:Open@7062,file:Open@738,file:Read@2720,file:ReadFile@1570,whole:Route@3033 752f5c566102d49a27c01c8d43ec2638a64b09b7b1868662ef652fca5f6d5f6a
internal/host/rootmigration_timers.go rootMigrationTimers.adopt authority G,J4 rev3=PROPOSED-p3;identity=offline migration root uid 0;basis=offline migration binds the exact observed pre-claim registrations to root retained stop evidence and denies; durable intention precedes conditional registry adoption and root inventory completion registry:ClaimOwner@1997,registry:ClaimOwner@2135,registry:ClaimOwnership@1860,registry:ClaimOwnership@1914,registry:ClaimOwnership@4009,registry:ClaimOwnership@4202,registry:ClaimOwnership@4314,registry:Host@1836,registry:Host@4107,registry:Name@1677,registry:Name@4122,registry:Registration@1153,registry:Registration@1785,registry:Registration@1810,registry:Registration@1962,registry:Registration@2117,registry:Registration@4046,whole:ClaimOwnership@1864,whole:ClaimOwnership@1918,whole:ClaimOwnership@1936,whole:ClaimOwnership@4015,whole:ClaimOwnership@4211,whole:ClaimOwnership@4320,whole:Route@2456,whole:Timer@1067,whole:Timer@1073,whole:Timer@1101,whole:Timer@1663,whole:Timer@2304,whole:Timer@3896,whole:Timer@3911,whole:Timer@3980,whole:Timer@3998,whole:Timer@4179,whole:Timer@4190,whole:Timer@4304,whole:Timer@4383,whole:Timer@4394 35914f3267326de830fb7c149bf330638cb49794a3e8c6f41445078a92669a04
internal/host/rootmigration_timers.go rootMigrationTimers.load authority G,J4 rev3=PROPOSED-p3;identity=offline migration root uid 0;basis=reads only the root-private saved adoption intention, requiring the same host, incarnation, stamp, session and enrolled box; preserves exact registration IDs on retry file:Read@145,registry:Registration@844,whole:Timer@668,whole:Timer@846 f58c918185b3c382e5c1545f441cb589157b0c6cf60bb54322ca7edf40f1c1c9
internal/host/rootmigration_timers.go rootMigrationTimers.key authority G,J4 rev3=PROPOSED-p3;identity=offline migration root uid 0;basis=scopes the durable timer adoption intention to the root-observed installation and denied session 76a349236e49d55a5b9866b86ea60aa56a7a5b179b7b77b82d87535f0a305f0c
@@ -201,7 +201,7 @@ internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory aut
@coverage internal/activator 0a01bc096c64f1886f810f4856d60f9a2642074a8b2ac0c56cbed77d122e76c8
@coverage internal/console 6f4998c39e6d66cd73ea98500000cbecac251dd21dafd58f51a85c2291e0c533
@coverage internal/edgesfile 7b48368150a427e4620f1c9ded49942b688c9e6c9cd983b7da43d3f2c0810b72
-@coverage internal/host 7d90a394b7feca2526fcfdfa593b2e794a3a5cbdf893e138b3420e6374edbf2f
+@coverage internal/host cec7e5d95134a61e8485d00a71e9ee1452d2f17938c46905092576f6a307dab5
@coverage internal/host/units bf6ae516012701e0ca41908167e4efd60dd3c2c24e9af1555dec077e8be44461
@coverage internal/hostbox f877b123ea1c5c361a3046f5962ea5ef8e1a3b8a534a29aa2c15aac507b37081
@coverage internal/hostfloor 696e5ea4fa37b924b68e8e262235b99298ee19342d875213ba8209e5287dd3c6
@@ -235,7 +235,7 @@ internal/rollout/rootfailures.go RootFailures.ImportLegacy authority E3 rev3=PRO
internal/rollout/rootfailures.go RootFailures.VerifiedRetry authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption uid 0;basis=verifies deployment-root signature, tuple, freshness and exact root book epoch before existing durable retry transition f4ab9edee069dedf8748055ee612a6ef8905b9770718c82378cfc15b0316810e
internal/rollout/rootfailures.go rootFailureWriter authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption and migration uid 0;basis=writes migration identity, failure epochs and import fingerprints atomically through checked private root store e32ffb3459fe2b13e0ecbe7b364bd405ef25325ed8cbd685b9ac1e076a8c197a
@coverage internal/rollout 56efcb8c1d7231a7f43d0464d97345930c83c2baf864762c1faa7a004286e984
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 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. 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;basis=wires the census-specific update authority under existing root release exclusion;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=combines E census-aware restart and bound-unit policy with P3 root-update settlement while keeping release exclusion outside participation, floor and registry locks whole:Route@11104,whole:Route@18654,whole:Route@18690,whole:Route@8406 8d4d89ecf22bf7144495d930b8580c00d4ab59a1cfc83620e4f59d27704b7bb0
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-p3;run=13;identity=root adopt uid 0;basis=selects the complete legacy proof/history path without a census; census participation gates history creation, program recording, refusals and mint policy together whole:Route@11104,whole:Route@18293,whole:Route@18329,whole:Route@8406 a25835004cca39d1c624fc47e59b7bcac47eb175b55c1db583fac920eaa33068
internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 PROPOSED E45 / R-E45-3: root checks census presence before opening release exclusion; absent census uses main adoption with no release directory or lock. Retained census remains protected with either flag value. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0;basis=passes the same census source to release exclusion and root settings to preserve the no-census branch;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=passes both the census probe and P3 production update opener under the common root release exclusion file:Open@176 13c055021f5e0453138bf773673521b86851eb16d567f6ccf968f5b7bc4f9199
internal/host/claimassembly.go NewProductionRootClaimStop authority C,E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=installs root filesystem identity capture in the production inventory handler;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures a55fcaa85155e2376d5f680ca8f86de6b579072e6a5c3ad47e1329b354311a00
internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1,E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=persists the root failure intent before launch and independently revalidates readiness and the exact unit invocation before served/request completion;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=retains P3 runtime settlement and E host-aware readiness validation under the generation reservation and host guard file:Read@4103 b19cb431a122d33703a83b39cb9a19786b7f509eba22961ec1e86d95f618e909
@@ -260,7 +260,7 @@ internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authorit
internal/host/rootinventory.go ClaimRootHTTP.verifyInventoryPath authority C,J1 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=uses checked persistent evidence for protected installation entries; explicit legacy fixtures preserve their same-boot check;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Lstat@315 2ee5519ed6e8ff27bcfdb5db7748a71f4f331a271d84d7688835798792824a78
internal/host/rootinventory_identity.go ClaimRootHTTP.bindInventoryPaths authority C,E2,J1 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=discards all caller-supplied persistent evidence and captures or revalidates it independently against the retained root inventory;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=discards caller proof, captures new objects independently, retains immutable prior proof, and verifies cross-boot ancestry through the shared inventory verifier;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires the proposed boot to equal the independently observed boot for new materialization, discards caller filesystem proof, captures new objects independently, retains immutable prior proof, and verifies cross-boot ancestry through the shared inventory verifier 73c94e6482dafa576ac607ba4a54a83921febf0f2092361e88a259a884db75d7
internal/host/rootinventory_state_linux.go ClaimRoleAssembly.prepareStateInventory authority C,J1 rev3=PROPOSED-p3;identity=carlos agent claim lifecycle uid carlos; final authorization remains in root;basis=uses root-supplied persistent state identity for remote inventory reuse while keeping agent proposals non-authoritative;rev3=PROPOSED-p3;run=11;identity=carlos agent uid carlos; root validates final inventory and launch;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstatat@1313,file:Fstatat@2304,file:Open@799,file:Openat@1086 c5355a26fa146eb0ddb4cc7c1e536de5781e171d52363a0be38ffb2074651a53
-internal/host/rootupdates_runtime.go beginRootGenerationUpdate authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=opens census-only root update authority, resolves the canonical host and refuses a known failure before runtime intent b060f3f53e87b9ea7bc78a6cd5d3435db63b07a83c3b0a38618d2481b000605e
+internal/host/rootupdates_runtime.go beginRootGenerationUpdate authority E2,E3 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=resolves canonical runtime update identity; sticky failure veto is disabled in production under the narrowed activation gate b060f3f53e87b9ea7bc78a6cd5d3435db63b07a83c3b0a38618d2481b000605e
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
@@ -271,7 +271,7 @@ internal/hostbox/coldhandover.go ColdHandover.Run authority C,J2,J4 rev3=PROPOSE
internal/hostbox/coldhandover.go ColdHandover.retireIntended 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=captures persistent identity for root-guarded empty directories before identity-conditional retirement file:Lstat@2251,registry:ClaimOwner@840,registry:Route@840,whole:ColdRows@842,whole:Route@849 85c7d0d2933bbd80f10690cddca7efa8920e789e35daeaa7e950dc78a352d71a
internal/hostbox/inventory_identity.go OpenInventoryFile authority C,E2,G rev3=PROPOSED-p3;identity=root claim retirement or offline migration uid 0; shared helpers also run for legacy agent requests without root authority;basis=opens inventory paths through no-follow checked directory and file descriptors for persistent identity verification file:OpenFile@116 07d0ccdc38099b5aec202a1e69332820897442ed8a803a8af679d4fb2e4d9e49
internal/hostbox/inventory_identity.go VerifyInventoryFile authority C,E2,G rev3=PROPOSED-p3;identity=root claim retirement or offline migration uid 0; shared helpers also run for legacy agent requests without root authority;basis=requires persistent root evidence and compares filesystem handle, ownership and stable mount attachment across boots;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Stat@81 965a8506c8255b8961a316c80f8fac4014933125e1507fa6e42121e9d3b3bb1b
-internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,E2 rev3=PROPOSED-p3;identity=carlos agent claim lifecycle uid carlos; final authorization remains in root;basis=preserves persistent root inventory evidence in exact retirement artifact requests;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3614,registry:ClaimIncarnation@3636,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2387,registry:ClaimOwner@2793,registry:ClaimOwner@3694,registry:ClaimOwner@3710,registry:ClaimOwner@3948,registry:ClaimOwnership@147,registry:ClaimRowID@3728,registry:ClaimRowID@3971,registry:ClaimStamp@1338,registry:ClaimStamp@362,registry:ClaimStamp@3660,registry:ClaimStamp@3676,registry:Host@1257,registry:Host@2292,registry:Host@2838,registry:Host@288,registry:Host@2895,registry:Host@3533,registry:Host@3916,registry:Name@3933,registry:Registration@3750,registry:Registration@3992,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3599 161f3654abb12c42f016704228b550d132c4e9da3d29906db4f908dc40994e4d
+internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,E2 rev3=PROPOSED-p3;run=13;identity=agent uid carlos; final retirement effects uid 0;basis=agent initiates retirement through root; root revalidates exact retained installation and persistent artifacts before destructive effects registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3614,registry:ClaimIncarnation@3636,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2387,registry:ClaimOwner@2793,registry:ClaimOwner@3694,registry:ClaimOwner@3710,registry:ClaimOwner@3948,registry:ClaimOwnership@147,registry:ClaimRowID@3728,registry:ClaimRowID@3971,registry:ClaimStamp@1338,registry:ClaimStamp@362,registry:ClaimStamp@3660,registry:ClaimStamp@3676,registry:Host@1257,registry:Host@2292,registry:Host@2838,registry:Host@288,registry:Host@2895,registry:Host@3533,registry:Host@3916,registry:Name@3933,registry:Registration@3750,registry:Registration@3992,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3599 161f3654abb12c42f016704228b550d132c4e9da3d29906db4f908dc40994e4d
internal/hostbox/typedpark_linux.go TypedParker.CaptureArtifact 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=captures filesystem handle, ownership and stable mount attachment when the root caller requests protected artifact capture;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstatat@440 8388e25cc65bd51896aef372e76dd9dd66947b3e67bbca0b76461b10742e17a2
internal/hostbox/typedpark_linux.go TypedParker.Park 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=requires persistent evidence for root-protected retirement and verifies source or replay destination before rename;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstat@1253,file:Fstat@1322,file:Fstatat@587 3b39fee2a35d1d711dbc59fe4ec0a091924d909547ea2303885b2cc8249e132b
internal/hostbox/typedpark_linux.go TypedParker.VerifyEmptyDirectory 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=requires protected persistent identity before using emptiness to retire an intended directory;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstat@661,file:Openat@439,file:Readdirnames@840 959832873e4daf74fbfa86d6e8139cc94d506545b59ef2dd1fa5059ac3c2374b
@@ -292,7 +292,7 @@ internal/caps/converge.go Restore authority E4,J4 rev3=PROPOSED-p3;run=10;identi
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;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=retains P3 census-wide mutation refusal and E per-host bound-unit refusal in the shared operation; no-census legacy path remains unchanged 3d9f360b51a05bf6a0525edb5afc747c6b7cc8d6c34b59638914237e488c998e
@coverage internal/caps 0502b0ee74b47a1a7b37e538e1c1c0ff30b50d08b5ec7e780b7429bf094c5eb6
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
-internal/host/claimactivation_preflight.go claimActivationPreflight authority G,J4 ;rev3=PROPOSED-p3;run=11;identity=claim construction in agent uid carlos and root adopt/instance-control uid 0;basis=before owners or claim state open, refuses every named-unit or backing=unit route while the single 4a completion constant is false; empty and ordinary registries remain permitted registry:Backing@280,registry:Unit@260,whole:Route@246 e5713a34359330ef311f31c47e0342f8322471243a7aba61fdecc751a53f34b4
+internal/host/claimactivation_preflight.go claimActivationPreflight authority G,J4 rev3=PROPOSED-p3;run=13;identity=agent uid carlos; root migration/adopt/control uid 0;basis=service-owned registry contributes refusal only; claimActivationVerified separately checks canonical records and root retained unit state registry:Backing@280,registry:Unit@260,whole:Route@246 e5713a34359330ef311f31c47e0342f8322471243a7aba61fdecc751a53f34b4
internal/host/rootgeneration_kernel_linux.go generationCurrentFiles authority E2 PROPOSED E51: Root reopens the canonical executable through trusted ancestry and its containing tree; persistent proof rebinds only the transient scanner copy to current devices. Historical pins without proof refuse across boots.;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=uses E persistent executable and tree proof; legacy numeric tuples require a nonempty equal current boot, never unknown or other-boot numbers file:Open@219 1c1a5ebdd68091af35b8e26cf07d20e623b54e7109a64cffd5a55017fd721fa4
internal/rootstate/state.go FileIdentity.Same authority E2 PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51: Persistent equality requires filesystem, handle, inode, ownership, mode and mount topology; device/mount numbers compare only on the same boot. PROPOSED E51 clarification: within one boot devices remain exact; mount IDs compare within one mount namespace. A root namespace clone still requires identical filesystem, handle, inode, ownership and mount topology.;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires boot and persistent filesystem, handle, inode, mode, ownership and attachment proof; same-boot device identity remains exact, mount numbers are local to the mount namespace; missing boot never grants equality 478f64ca9e22e9ed33b655541ccb487289405a1b817b3789a6771650d3d0b7b4
@removed internal/host/rootgeneration_kernel_linux.go generationCurrentBinding A,E2,G census-ruling-J4
@@ -305,3 +305,26 @@ internal/rootstate/state.go FileIdentity.Same authority E2 PROPOSED E51: Persist
internal/host/claimwakeassembly.go type ClaimLaunchInputs authority C,J4 rev3=PROPOSED-p3;run=12;basis=agent launch selection; preserves E named-unit inputs and seed own-write CAS callback together f64420b0bc9f2479961d965b297b86274ef69b31894b0dbe056e19850eb5c3b8
internal/host/movesfiles.go movesFiles.Establish authority C,J4 rev3=PROPOSED-p3;run=12;basis=agent files preparation; retains seed admitted-writer and ordinary origin proof with one C8 commit; H3 is inventory bookkeeping, C owns the enabled effect 172fe8888da88c249b7990bd9fb4bd2821bd402fb96ab52af13c58500f31d50f
internal/hostbox/servinghandover.go ServingHandover.Run authority C,J4 rev3=PROPOSED-p3;run=12;identity=root handover controller uid 0;basis=preserves seed fresh boot-aware directory verification and persists its checked full filesystem identity in successor inventory only; immutable predecessor retained registry:Account@2154,registry:Addr@2373,registry:Addrs@2391,registry:App@2198,registry:Backing@2410,registry:Canary@2467,registry:ClaimOwner@5643,registry:ClaimOwnership@2841,registry:ClaimOwnership@3057,registry:ClaimOwnership@3090,registry:ClaimRowID@3120,registry:ClaimRowID@5714,registry:ClaimRowID@5888,registry:DBPath@2286,registry:Kind@2128,registry:Name@3174,registry:Name@5853,registry:Platform@2451,registry:Replica@2481,registry:Route@2841,registry:Route@3090,registry:Route@5643,registry:Route@5714,registry:Socket@2234,registry:Timers@3039,registry:Timers@3057,registry:Timers@3120,registry:Timers@3174,registry:Timers@5796,registry:Tunnel@2431,registry:Unit@2355,whole:ClaimOwnership@2853,whole:ClaimOwnership@3073,whole:ClaimOwnership@3102,whole:ColdRows@3514,whole:ColdRows@3522,whole:ColdRows@3530,whole:ColdRows@3537,whole:ColdRows@5645,whole:ColdRows@5716,whole:ColdRows@5798,whole:ColdRows@6379,whole:ColdRows@6389,whole:Route@2114,whole:Route@2643,whole:Route@2651,whole:Route@2701,whole:Route@2709,whole:Route@2847,whole:Route@3096,whole:Route@5651,whole:Route@5722,whole:Timer@2656,whole:Timer@2664,whole:Timer@2714,whole:Timer@2752,whole:Timer@3045,whole:Timer@3063,whole:Timer@3126,whole:Timer@3180,whole:Timer@5804 c1367b936e3e0caa9e2bbd0da117a9b89ce3b7b7d4f518069e7d265a48b89a04
+internal/host/claimactivation_preflight.go claimActivationVerified authority G rev3=PROPOSED-p3;run=13;identity=agent uid carlos; root migration/adopt/control uid 0;basis=reads strict canonical deployment records in this placement scope; root additionally refuses retained unit configuration, bindings or materializations while 4a is incomplete cec48f637f31d76872d4d27c5e4391d8423c44de00a1bbd4aac63dcb70baa063
+internal/host/claimactivation_preflight.go var openClaimActivationState authority G rev3=PROPOSED-p3;run=13;identity=root migration/adopt/control uid 0;basis=opens retained root unit namespaces read-only without creating claim state file:Open@31 9b4c4aeddcc3756f3c7fbb267096147f5670511fc4fda809d88d98540ad685f6
+internal/host/claimstop_run.go RunInstanceControl authority C,D,G rev3=PROPOSED-p3;run=13;identity=instance-control uid 0;basis=requires retained recovery census before construction and complete launch verification after pending recovery before serving d55919f2c48d02f39bb2d0eda76c7854aaf2c7684b8c782483d8fb62e5f5bff4
+internal/host/claimstop_run.go claimRecoveryReady authority C,G rev3=PROPOSED-p3;run=13;identity=instance-control uid 0;basis=requires valid retained recovery census but defers complete launch inventory until restricted recovery has settled 627ab800a8f5772b81c3a893cf546438d7f76ae30ed29949ae6af52b5c2a9a7b
+internal/host/claimunit_binding.go claimUnitBindings.establish authority E2 rev3=PROPOSED-p3;run=13;identity=root unit controller uid 0;basis=production admission refuses binding while named-unit 4a is incomplete, including backing=unit ef56f2db27a3d4a95803407a4b389b402592ed5b450bb6723aeffac42a041cab
+internal/host/claimunit_dispatch.go claimUnitDispatch.forStart authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=verifies effective ordinary templates as well as retained named bindings before start and at the reservation permit recheck 3d358cb14cd248cea66750f64a4ea49be91eddb563e6ca389c12c7664f6c6706
+internal/host/claimunit_dropin_linux.go openClaimUnitDropin authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=opens only the exact reservation drop-in under checked root ancestry for valid named or ordinary run units file:Fstat@694,file:Fstat@984,file:Open@290,file:Openat@496 f9a9915d67e7f8e4a540a484a7c978bf2d22eb803c04564349f9db5df119de6b
+internal/host/claimunit_dropin_other.go openClaimUnitDropin authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=opens only the exact reservation drop-in under checked root ancestry for valid named or ordinary run units bb3a29d1ad8840139f7236971ecb2bae93e6323007f2fdd625114bb3ff105397
+internal/host/claimunit_effective.go claimEffectiveUnit authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=ordinary launches require exact embedded template bytes, only the reservation drop-in and checked effective systemd identity, capabilities and containment; bound launches retain definition checks file:ReadAll@3614,file:ReadAll@643,file:ReadFile@457 7f02ae5a7047e09fffb5ad8dd8fd5e4336236f5f5128b77d41bef043171b66eb
+internal/host/claimunit_effective.go claimReservedDropin authority E2 rev3=PROPOSED-p3;run=13;identity=root launch controller uid 0;basis=derives exact host reservation mounts and trampoline arguments; ordinary units retain systemd instance expansion data-literal@382 bf9e5d43b589d965ee9d9af968610bab7342e274eba33dcea3a1f76c3bda50d3
+internal/host/claimunit_files_linux.go projectClaimBoundSocket authority E2 rev3=PROPOSED-p3;run=13;identity=root unit materializer uid 0;basis=selects and publishes the root-bound socket symlink and synchronizes its parent; this is publication authority file:Readlinkat@433 4f3c8daab1f68102e7be9511d1ec29d89c584c99aed79641f2b740031e503ab4
+internal/host/claimunit_files_other.go projectClaimBoundSocket authority E2 rev3=PROPOSED-p3;run=13;identity=root unit materializer uid 0;basis=selects and publishes the root-bound socket symlink and synchronizes its parent; this is publication authority 3f72016917b749fed665db738fbb54d90b40f59eec09aa3751b6abbb2f14319a
+internal/host/claimunit_materialize.go claimUnitMaterializer.install authority E2 rev3=PROPOSED-p3;run=13;identity=root unit controller uid 0;basis=production admission refuses materialization while named-unit 4a is incomplete, including backing=unit ed99fda682170367aee0a3d6270153b7051ae03849aa93dc24ecca60eb7afec4
+internal/host/claimunit_production.go bindProductionClaimUnits authority E2 rev3=PROPOSED-p3;run=13;identity=root control startup uid 0;basis=installs effective verification for ordinary and bound starts; incomplete 4a refuses production binding and materialization for named and backing units file:Read@1275 e4b9166fe33bcbafe91efb09d99d139d1f697df35dd7cff6765c9e483972d4c5
+internal/host/proofroot.go configureRootProof authority E1,B3 rev3=PROPOSED-p3;run=13;identity=root adopt uid 0;basis=checks participation before opening history or installing root proof policy; errors deny minting without falling back 322c7da5d034cd1cc870e748c499864796fa8bc11e4bbed6f96f7312d004dbd1
+internal/host/proofroot.go proofRoot.mintAndLog authority E1 rev3=PROPOSED-p3;run=13;identity=root adopt uid 0;basis=retains main minting only when no root policy callbacks are installed; partial census policy without a probe refuses, and a complete policy independently checks participation file:Stat@2049,registry:App@1052,registry:App@1190,registry:App@893,registry:Host@1107,registry:Host@810,registry:Host@931,registry:Unit@763,registry:Unit@780,whole:Route@1000,whole:Route@723,whole:Route@756,whole:Route@868,whole:Route@965 faf5c143825d0f2d263c27d0722fba407b387f0a814b0e8a7212a82e813e240a
+internal/host/roothistory_creation.go productionCreationHistory authority E1 rev3=PROPOSED-p3;run=13;identity=root controller uid 0;basis=reuses the production release-exclusion token during recovery before recording independently checked installation creation file:Open@297 495a357c0a2a1354d5c5510744f9bf0778034bd6d7b0d68fa1881791e997634f
+internal/host/rootinventory_startup.go recoverRootInventoryStartup authority C,G rev3=PROPOSED-p3;run=13;identity=root control startup and system recovery uid 0;basis=validates retained migration identity before constructing a restricted inventory-only recovery handler; replays root-retained intents and refuses construction until no pending intent remains file:Open@449 fd85ecea2daed0933d8facd17eefed2a0d811c2d50cf6a1bd8dfcfa95c8ba8ec
+internal/host/rootmigration_production_linux.go quiesceLegacyRootMutators authority G rev3=PROPOSED-p3;run=13;identity=offline migration uid 0;basis=under mandatory release exclusion scans root process command lines and refuses already-running legacy adopt/apply/guard-arm before census creation file:ReadDir@77,file:ReadFile@537,file:Stat@381 5b8d7964c595634d80699448aa5f052fc2e8c215e3c9bd8549378d37ce25d2ca
+internal/host/rootmigration_startup.go readRootMigrationState authority G rev3=PROPOSED-p3;run=13;identity=root startup/recovery uid 0;basis=validates retained census box/enrollment/generation and complete migration record before restricted recovery; grants no launch file:Read@425 9a8ddf2b605833b7fa4c0908ade98a6fc2efbe87c26e92ede289bbcc1d05beec
+internal/host/rootmovesfloor_boot.go sameRootFloorImage authority F rev3=PROPOSED-p3;run=13;identity=root boot/recovery uid 0;basis=compares freshly verified signed digest, release and protocols, not saved boot-local file numbers 532078238827331f9282d85d1e17e146ed4842a215db057238aa5f6cb40ba137
+internal/host/rootreleaselock.go withClaimReleaseCensus authority E2 rev3=PROPOSED-p3;run=13;identity=root adopt/guard uid 0;basis=census paths require release exclusion; legacy paths use an already-created migration exclusion without creating state 514b5e479c5340b02ec78106cdc19b90a03ec4a3ac229eb82c654f09bc6a92e5
+internal/host/rootreleaselock.go withExistingRootRelease authority G rev3=PROPOSED-p3;run=13;identity=root legacy adopt/update uid 0;basis=unchanged legacy effect when migration exclusion does not exist; otherwise serializes with explicit migration before any mutation file:Lstat@93,file:Open@225 be32aad37d2b0a17979c74ed097d00728da6fedefd30862b8ec5537745bf55ae