carlos / platform Public

Claim activation tools, run 4: restore ordering (not for merge)

Restore keeps the edge persistently masked and adoption triggers excluded until every other unit is reconciled, records completion, then starts the edge last; 46 reboot checkpoints and real ensureSystemUnits witnesses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev ac70dab67449bf568d985e94a19df24cb4751be7 parent 03111ef
9 files changed, +778 −137
  • internal/host/activation_adoption_linux.go +38 −0
  • internal/host/activation_adoption_other.go +5 −0
  • internal/host/activation_offline_linux.go +60 −71
  • internal/host/activation_offline_rootrunner_linux_test.go +20 −61
  • internal/host/activation_restore_order_rootrunner_linux_test.go +634 −0
  • internal/host/agent.go +3 −0
  • internal/host/systemapply.go +3 −0
  • internal/host/systemunits.go +4 −0
  • internal/provision/testdata/plan14-root-reads-activation-tools.tsv +11 −5

CI passed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/superfly/ltx v0.5.1
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading golang.org/x/crypto v0.53.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading golang.org/x/net v0.56.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading golang.org/x/text v0.38.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
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 ./...
go: downloading golang.org/x/tools v0.47.0
go: downloading golang.org/x/mod v0.37.0
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	293.689s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.826s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.309s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	249.736s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.440s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.129s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.921s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.699s
ok  	github.com/carlosframework/platform/internal/changelog	0.304s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.022s
ok  	github.com/carlosframework/platform/internal/clamd	0.059s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	390.753s
?   	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.152s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.318s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	27.347s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.130s
ok  	github.com/carlosframework/platform/internal/gate	1.658s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.020s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.150s
ok  	github.com/carlosframework/platform/internal/host	175.108s
ok  	github.com/carlosframework/platform/internal/hostactors	0.345s
ok  	github.com/carlosframework/platform/internal/hostbox	2.559s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.502s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	5.116s
ok  	github.com/carlosframework/platform/internal/hostfence	0.036s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.949s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.041s
ok  	github.com/carlosframework/platform/internal/hostops	1.529s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.015s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.015s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.433s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.129s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.016s
ok  	github.com/carlosframework/platform/internal/infraview	0.015s
ok  	github.com/carlosframework/platform/internal/instancectl	5.267s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.729s
ok  	github.com/carlosframework/platform/internal/instancefs	0.020s
ok  	github.com/carlosframework/platform/internal/instancekey	0.016s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.026s
ok  	github.com/carlosframework/platform/internal/layout	1.535s
ok  	github.com/carlosframework/platform/internal/lease	9.877s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.011s
ok  	github.com/carlosframework/platform/internal/mail	0.160s
ok  	github.com/carlosframework/platform/internal/maintenance	0.085s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.545s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.065s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	21.528s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.785s
ok  	github.com/carlosframework/platform/internal/objstore	0.173s
ok  	github.com/carlosframework/platform/internal/ops	0.169s
ok  	github.com/carlosframework/platform/internal/park	0.197s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	83.920s
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.751s
ok  	github.com/carlosframework/platform/internal/relay	7.302s
ok  	github.com/carlosframework/platform/internal/release	22.894s
ok  	github.com/carlosframework/platform/internal/rollout	3.750s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.006s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.568s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.490s
ok  	github.com/carlosframework/platform/internal/seeds	0.012s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.245s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.135s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.220s
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.003s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.003s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.562s
=== 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: 48693410 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/16 17:18:25 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF122003938354, 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.020s
minio contract: PASS
diff --git a/internal/host/activation_adoption_linux.go b/internal/host/activation_adoption_linux.go
new file mode 100644
index 000000000..91c261a2c
--- /dev/null
+++ b/internal/host/activation_adoption_linux.go
@@ -0,0 +1,38 @@
+//go:build linux
+
+package host
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+)
+
+// An explicit offline transaction excludes automatic unit repair through edge startup.
+// A process lock alone would release that exclusion on interruption or reboot.
+func activationAdoptionAllowed(etc string) error {
+ statePath := filepath.Join(etc, "carlos", "claim-activation", "offline")
+ if _, err := os.Lstat(statePath); errors.Is(err, os.ErrNotExist) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ state, err := activationDir(statePath, false)
+ if err != nil {
+ return err
+ }
+ defer state.Close()
+ st, err := state.Stat()
+ if err != nil || st.Mode().Perm() != 0700 {
+ return errActivation
+ }
+ tx := activationOffline{units: filepath.Join(etc, "systemd", "system"), state: statePath}
+ r, err := tx.read(state)
+ if err != nil {
+ return err
+ }
+ if r.Phase != "done" {
+ return errActivation
+ }
+ return nil
+}
diff --git a/internal/host/activation_adoption_other.go b/internal/host/activation_adoption_other.go
new file mode 100644
index 000000000..40ec0c8bb
--- /dev/null
+++ b/internal/host/activation_adoption_other.go
@@ -0,0 +1,5 @@
+//go:build !linux
+
+package host
+
+func activationAdoptionAllowed(string) error { return nil }
diff --git a/internal/host/activation_offline_linux.go b/internal/host/activation_offline_linux.go
index 914b1e4ec..5b68264e0 100644
--- a/internal/host/activation_offline_linux.go
+++ b/internal/host/activation_offline_linux.go
@@ -17,12 +17,14 @@ import (
"github.com/carlosframework/platform/internal/safefile"
)
+var activationEdgeUnit = "carlos-edge.service"
+
var activationTriggers = []string{"carlos-adopt.timer", "carlos-adopt.path", "carlos-stage.path", "carlos-restart.path", "carlos-system-update.path"}
func activationOfflineUnits() []string {
out := append([]string(nil), activationTriggers...)
out = append(out, rootMigrationOfflineUnits...)
- return append(out, "carlos-edge.service")
+ return append(out, activationEdgeUnit)
}
type activationOffline struct {
@@ -175,7 +177,7 @@ func (t activationOffline) read(state *os.File) (activationOfflineRecord, error)
}
}
switch r.Phase {
- case "prepared", "held", "seed-prepared", "restoring", "released", "done":
+ case "prepared", "held", "seed-prepared", "restoring", "released", "complete", "done":
default:
return r, errActivation
}
@@ -417,11 +419,7 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
if r.Phase == "done" {
return nil
}
- pending := r.Items
- if r.Phase == "released" {
- pending = append(append([]activationOfflineItem(nil), r.Items[:len(activationTriggers)]...), r.Items[len(r.Items)-1])
- }
- for _, item := range pending {
+ for _, item := range r.Items {
if err = t.checkItem(item); err != nil {
return err
}
@@ -429,38 +427,7 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
if dry {
return nil
}
- if r.Phase != "released" {
- r.Phase = "restoring"
- if err = t.save(state, r); err != nil {
- return err
- }
- for _, item := range r.Items[:len(r.Items)-1] {
- if err = t.replace(units, state, item, true); err != nil {
- return err
- }
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("restore-reload"); err != nil {
- return err
- }
- // Once triggers run, adopt may replace a restored unit. Relinquish those
- // service files durably before starting any trigger. Trigger files and
- // their active states remain ours until done, as does the edge.
- r.Phase = "released"
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- reconcile := func(item activationOfflineItem, owned bool) error {
- if owned {
- if err := t.checkItem(item); err != nil {
- return err
- }
- } else if !item.Active || item.Masked {
- return nil
- }
+ reconcile := func(item activationOfflineItem) error {
verb := "stop"
if item.Active && !item.Masked {
verb = "start"
@@ -487,48 +454,73 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
}
return nil
}
- // Stop originally inactive triggers before starting any of the others.
- for _, item := range r.Items[:len(activationTriggers)] {
- if !item.Active || item.Masked {
- if err = reconcile(item, true); err != nil {
- return err
- }
- }
- }
- for i, item := range r.Items[:len(r.Items)-1] {
- if i < len(activationTriggers) && (!item.Active || item.Masked) {
- continue
+ edge := r.Items[len(r.Items)-1]
+ // The edge mask survives every pre-completion checkpoint, including reboot.
+ // Adoption entrypoints also honor this journal until the final done record.
+ if r.Phase != "complete" {
+ if err = t.replace(units, state, edge, false); err != nil {
+ return err
}
- if err = reconcile(item, i < len(activationTriggers)); err != nil {
+ r.Phase = "restoring"
+ if err = t.save(state, r); err != nil {
return err
}
- }
- verifyTriggers := func() error {
for _, item := range r.Items[:len(activationTriggers)] {
- if err := t.checkItem(item); err != nil {
+ if _, err = t.systemctl(ctx, "stop", item.Name); err != nil {
return err
}
- load, active, err := t.status(ctx, item.Name)
- if err != nil {
+ if err = t.point("hold-" + item.Name); err != nil {
return err
}
- wantLoad, wantActive := "loaded", "inactive"
- if item.Masked {
- wantLoad = "masked"
+ }
+ for _, item := range r.Items[len(activationTriggers) : len(r.Items)-1] {
+ if err = t.replace(units, state, item, true); err != nil {
+ return err
}
- if item.Active && !item.Masked {
- wantActive = "active"
+ }
+ if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
+ return err
+ }
+ if err = t.point("restore-reload"); err != nil {
+ return err
+ }
+ for _, item := range r.Items[len(activationTriggers) : len(r.Items)-1] {
+ if err = reconcile(item); err != nil {
+ return err
}
- if load != wantLoad || active != wantActive {
- return errActivation
+ }
+ r.Phase = "released"
+ if err = t.save(state, r); err != nil {
+ return err
+ }
+ for _, item := range r.Items[:len(activationTriggers)] {
+ if err = t.replace(units, state, item, true); err != nil {
+ return err
}
}
- return nil
+ if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
+ return err
+ }
+ if err = t.point("trigger-reload"); err != nil {
+ return err
+ }
}
- if err = verifyTriggers(); err != nil {
- return err
+ for _, active := range []bool{false, true} {
+ for _, item := range r.Items[:len(activationTriggers)] {
+ if (item.Active && !item.Masked) == active {
+ if err = reconcile(item); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ if r.Phase != "complete" {
+ // Save completion before releasing the persistent edge mask.
+ r.Phase = "complete"
+ if err = t.save(state, r); err != nil {
+ return err
+ }
}
- edge := r.Items[len(r.Items)-1]
if err = t.replace(units, state, edge, true); err != nil {
return err
}
@@ -538,10 +530,7 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
if err = t.point("edge-reload"); err != nil {
return err
}
- if err = reconcile(edge, true); err != nil {
- return err
- }
- if err = verifyTriggers(); err != nil {
+ if err = reconcile(edge); err != nil {
return err
}
r.Phase = "done"
diff --git a/internal/host/activation_offline_rootrunner_linux_test.go b/internal/host/activation_offline_rootrunner_linux_test.go
index 2a522d380..d71932438 100644
--- a/internal/host/activation_offline_rootrunner_linux_test.go
+++ b/internal/host/activation_offline_rootrunner_linux_test.go
@@ -266,8 +266,24 @@ func TestRootActivationOfflineEdgeLast(t *testing.T) {
}
restoring = true
started := map[string]bool{}
+ reconciled := map[string]bool{}
tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] == "stop" {
+ reconciled[args[1]] = true
+ }
if args[0] == "start" {
+ for _, trigger := range activationTriggers {
+ if args[1] == trigger {
+ for _, service := range rootMigrationOfflineUnits {
+ if !reconciled[service] {
+ t.Fatalf("started %s before reconciling %s", trigger, service)
+ }
+ }
+ }
+ }
+ if args[1] == "carlos-edge.service" && offlineRecord(t, tx).Phase != "complete" {
+ t.Fatal("edge started before durable completion")
+ }
if args[1] != "carlos-edge.service" {
target, _ := os.Readlink(filepath.Join(tx.units, "carlos-edge.service"))
if target != "/dev/null" {
@@ -309,63 +325,6 @@ func TestRootActivationProcessDrain(t *testing.T) {
}
}
-func TestRootActivationOfflineResumeAfterTriggerRepair(t *testing.T) {
- tx, _ := offlineFixture(t)
- original := tx.systemctl
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "show" && args[1] == "carlos-adopt.timer" {
- return []byte("LoadState=loaded\nActiveState=active\n"), nil
- }
- return original(ctx, args...)
- }
- units, state, err := tx.dirs(true)
- if err != nil {
- t.Fatal(err)
- }
- _, err = tx.prepare(t.Context(), units, state)
- units.Close()
- state.Close()
- if err != nil {
- t.Fatal(err)
- }
- tx.systemctl = original
- if err = tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- injected := errors.New("lost trigger start reply")
- once := false
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "start" && args[1] == "carlos-adopt.timer" {
- if !once {
- once = true
- // An enabled adopt trigger can repair other units immediately.
- path := filepath.Join(tx.units, "carlos-system-update.service")
- if err := os.WriteFile(path+".repair", []byte("[Unit]\nDescription=repaired by adopt\n"), 0644); err != nil {
- t.Fatal(err)
- }
- if err := os.Rename(path+".repair", path); err != nil {
- t.Fatal(err)
- }
- return nil, injected
- }
- }
- if args[0] == "show" && args[1] == "carlos-adopt.timer" {
- return []byte("LoadState=loaded\nActiveState=active\n"), nil
- }
- return original(ctx, args...)
- }
- if err = tx.restore(t.Context(), false); !errors.Is(err, injected) {
- t.Fatal("failure not reached", err)
- }
- if err = tx.restore(t.Context(), false); err != nil {
- t.Fatal("released unit blocks restore", err)
- }
- raw, _ := os.ReadFile(filepath.Join(tx.units, "carlos-system-update.service"))
- if !strings.Contains(string(raw), "repaired by adopt") {
- t.Fatal("restored over new owner")
- }
-}
-
func TestRootActivationRestoreInactiveTrigger(t *testing.T) {
for _, point := range []string{"restore-reload", "journal-released"} {
t.Run(point, func(t *testing.T) {
@@ -388,8 +347,8 @@ func TestRootActivationRestoreInactiveTrigger(t *testing.T) {
t.Fatal(err)
}
tx.checkpoint = nil
- if out, err := tx.systemctl(t.Context(), "start", "carlos-adopt.timer"); err != nil {
- t.Fatal(string(out), err)
+ if out, err := tx.systemctl(t.Context(), "start", "carlos-adopt.timer"); err == nil {
+ t.Fatal("adoption timer unmasked before service reconciliation", string(out))
}
if err := tx.restore(t.Context(), false); err != nil {
t.Fatal(err)
@@ -472,8 +431,8 @@ func TestRootRebootActivationRestorePhase2(t *testing.T) {
t.Fatal("no actual reboot", err)
}
_, active, err := tx.status(t.Context(), "carlos-adopt.timer")
- if err != nil || active != "active" {
- t.Fatal("enabled timer did not start on reboot", active, err)
+ if err != nil || active != "inactive" {
+ t.Fatal("held timer ran on reboot", active, err)
}
if err := tx.restore(t.Context(), false); err != nil {
t.Fatal(err)
diff --git a/internal/host/activation_restore_order_rootrunner_linux_test.go b/internal/host/activation_restore_order_rootrunner_linux_test.go
new file mode 100644
index 000000000..6e8ea3c6f
--- /dev/null
+++ b/internal/host/activation_restore_order_rootrunner_linux_test.go
@@ -0,0 +1,634 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func offlineRecord(t *testing.T, tx activationOffline) activationOfflineRecord {
+ t.Helper()
+ raw, err := os.ReadFile(filepath.Join(tx.state, "transaction.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var r activationOfflineRecord
+ if err := json.Unmarshal(raw, &r); err != nil {
+ t.Fatal(err)
+ }
+ return r
+}
+
+func TestRootActivationRestoreEveryBoundaryBoot(t *testing.T) {
+ for cut := 1; cut < 160; cut++ {
+ reached := false
+ t.Run(fmt.Sprint(cut), func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ base := tx.systemctl
+ active := map[string]bool{"carlos-edge.service": true}
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ switch args[0] {
+ case "start":
+ active[args[1]] = true
+ case "stop":
+ active[args[1]] = false
+ }
+ raw, err := base(ctx, args...)
+ if args[0] == "show" && active[args[1]] {
+ raw = []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState=active"))
+ }
+ return raw, err
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ count := 0
+ stop := errors.New("power loss")
+ point := ""
+ tx.checkpoint = func(name string) error {
+ count++
+ r := offlineRecord(t, tx)
+ if r.Phase != "complete" && r.Phase != "done" {
+ if target, _ := os.Readlink(filepath.Join(tx.units, "carlos-edge.service")); target != "/dev/null" {
+ t.Fatalf("edge unmasked before complete at %s", name)
+ }
+ }
+ if count == cut {
+ reached = true
+ point = name
+ return stop
+ }
+ return nil
+ }
+ if err := tx.restore(t.Context(), false); err != nil && !errors.Is(err, stop) {
+ t.Fatal(err)
+ }
+ tx.checkpoint = nil
+ // Model boot from persistent masks, including an enabled but inactive timer.
+ for _, name := range activationOfflineUnits() {
+ target, _ := os.Readlink(filepath.Join(tx.units, name))
+ active[name] = target != "/dev/null" && (name == "carlos-adopt.timer" || name == "carlos-edge.service")
+ }
+ r := offlineRecord(t, tx)
+ if r.Phase != "complete" && r.Phase != "done" && active["carlos-edge.service"] {
+ t.Fatalf("boot served at %s", point)
+ }
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatalf("resume at %s: %v", point, err)
+ }
+ if !active["carlos-edge.service"] {
+ t.Fatal("edge not restored")
+ }
+ if r.Phase != "done" && active["carlos-adopt.timer"] {
+ t.Fatalf("inactive timer not reconciled after %s", point)
+ }
+ })
+ if !reached {
+ break
+ }
+ }
+}
+
+func TestRootActivationRestoreRealEnsureInterrupted(t *testing.T) {
+ tx, d := offlineFixture(t)
+ etc := filepath.Join(d, "etc")
+ units := filepath.Join(etc, "systemd", "system")
+ if err := os.MkdirAll(filepath.Dir(units), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(tx.units, units); err != nil {
+ t.Fatal(err)
+ }
+ tx.units = units
+ tx.state = filepath.Join(etc, "carlos", "claim-activation", "offline")
+ active := map[string]bool{"carlos-adopt.timer": true, "carlos-edge.service": true}
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ switch args[0] {
+ case "stop":
+ active[args[1]] = false
+ case "start":
+ active[args[1]] = true
+ case "show":
+ load, state := "loaded", "inactive"
+ if target, _ := os.Readlink(filepath.Join(tx.units, args[1])); target == "/dev/null" {
+ load = "masked"
+ }
+ if active[args[1]] {
+ state = "active"
+ }
+ return []byte("LoadState=" + load + "\nActiveState=" + state + "\n"), nil
+ }
+ return nil, nil
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ stop := errors.New("lost adoption timer start reply")
+ tx.checkpoint = func(name string) error {
+ if name == "start-carlos-adopt.timer" {
+ return stop
+ }
+ return nil
+ }
+ if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
+ t.Fatal(err)
+ }
+ path := filepath.Join(tx.units, "carlos-system-update.path")
+ before, err := os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ calls := 0
+ ctl := func(args ...string) (string, error) { calls++; return "", nil }
+ probe := func(args ...string) (string, error) { return "inactive", nil }
+ ensureSystemUnits(etc, filepath.Join(d, "apply.lock"), ctl, probe, t.Logf)
+ after, err := os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !os.SameFile(before, after) || calls != 0 {
+ t.Fatalf("real ensureSystemUnits ran during interrupted restore: same inode=%v commands=%d", os.SameFile(before, after), calls)
+ }
+ tx.checkpoint = nil
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ ensureSystemUnits(etc, filepath.Join(d, "apply.lock"), ctl, probe, t.Logf)
+ after, err = os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if os.SameFile(before, after) || calls == 0 {
+ t.Fatal("real ensureSystemUnits did not resume after restore")
+ }
+}
+
+func offlineMatrixNames(t *testing.T, n int) {
+ t.Helper()
+ activationEdgeUnit = fmt.Sprintf("carlos-activation-%03d-edge.service", n)
+ activationTriggers = []string{}
+ for _, name := range []string{"adopt.timer", "adopt.path", "stage.path", "restart.path", "system-update.path"} {
+ activationTriggers = append(activationTriggers, fmt.Sprintf("carlos-activation-%03d-%s", n, name))
+ }
+ rootMigrationOfflineUnits = []string{}
+ for _, name := range []string{"adopt.service", "stage.service", "config-sync.service", "restart.service", "system-update.service", "system-recover.service"} {
+ rootMigrationOfflineUnits = append(rootMigrationOfflineUnits, fmt.Sprintf("carlos-activation-%03d-%s", n, name))
+ }
+}
+
+func offlineMatrixGlobals(t *testing.T) {
+ t.Helper()
+ edge, triggers, services := activationEdgeUnit, activationTriggers, rootMigrationOfflineUnits
+ t.Cleanup(func() { activationEdgeUnit, activationTriggers, rootMigrationOfflineUnits = edge, triggers, services })
+}
+
+type offlineMatrixCut struct {
+ N int
+ Point, Phase string
+}
+
+func TestRootRebootActivationRestoreMatrixPhase1(t *testing.T) {
+ offlineMatrixGlobals(t)
+ if err := os.Mkdir(offlineRebootState, 0700); err != nil {
+ t.Fatal(err)
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(offlineRebootState+"/boot", boot, 0600); err != nil {
+ t.Fatal(err)
+ }
+ cuts := []offlineMatrixCut{}
+ for n := 1; n < 160; n++ {
+ offlineMatrixNames(t, n)
+ tx := activationOffline{units: "/etc/systemd/system", state: fmt.Sprintf("%s/case-%03d", offlineRebootState, n), systemctl: offlineRealSystemctl, drained: func(context.Context) error { return nil }}
+ for _, name := range activationOfflineUnits() {
+ path := filepath.Join(tx.units, name)
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("pre-existing", path, err)
+ }
+ raw := "[Unit]\nDescription=activation reboot matrix\n[Service]\nExecStart=/usr/bin/sleep infinity\n[Install]\nWantedBy=multi-user.target\n"
+ if strings.HasSuffix(name, ".timer") {
+ raw = "[Unit]\nDescription=activation reboot timer\n[Timer]\nOnActiveSec=1h\n[Install]\nWantedBy=timers.target\n"
+ }
+ if strings.HasSuffix(name, ".path") {
+ target := strings.TrimSuffix(name, ".path") + ".service"
+ if strings.HasSuffix(name, "adopt.path") {
+ target = strings.TrimSuffix(name, "adopt.path") + "config-sync.service"
+ }
+ raw = "[Unit]\nDescription=activation reboot path\n[Path]\nPathExists=" + offlineRebootState + "/never\nUnit=" + target + "\n[Install]\nWantedBy=multi-user.target\n"
+ }
+ if strings.HasSuffix(name, "-stage.service") {
+ err = os.Symlink("/dev/null", path)
+ } else {
+ err = os.WriteFile(path, []byte(raw), 0644)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, args := range [][]string{{"daemon-reload"}, {"enable", "--now", activationEdgeUnit}, {"enable", activationTriggers[0]}} {
+ if out, err := tx.systemctl(t.Context(), args...); err != nil {
+ t.Fatal(string(out), err)
+ }
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(n, err)
+ }
+ count := 0
+ cut := offlineMatrixCut{N: n}
+ stop := errors.New("reboot cut")
+ tx.checkpoint = func(name string) error {
+ count++
+ r := offlineRecord(t, tx)
+ if r.Phase != "complete" && r.Phase != "done" {
+ if target, _ := os.Readlink(filepath.Join(tx.units, activationEdgeUnit)); target != "/dev/null" {
+ t.Fatalf("%d: edge unmasked at %s before complete", n, name)
+ }
+ }
+ if count == n {
+ cut.Point = name
+ return stop
+ }
+ return nil
+ }
+ err = tx.restore(t.Context(), false)
+ if err != nil && !errors.Is(err, stop) {
+ t.Fatal(n, err)
+ }
+ cut.Phase = offlineRecord(t, tx).Phase
+ cuts = append(cuts, cut)
+ t.Logf("actual reboot fixture %d: %s phase=%s", n, cut.Point, cut.Phase)
+ if err == nil {
+ break
+ }
+ }
+ raw, err := json.Marshal(cuts)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = os.WriteFile(offlineRebootState+"/cuts.json", raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRootRebootActivationRestoreMatrixPhase2(t *testing.T) {
+ offlineMatrixGlobals(t)
+ before, err := os.ReadFile(offlineRebootState + "/boot")
+ if err != nil {
+ t.Fatal(err)
+ }
+ after, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil || string(before) == string(after) {
+ t.Fatal("no actual reboot", err)
+ }
+ raw, err := os.ReadFile(offlineRebootState + "/cuts.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var cuts []offlineMatrixCut
+ if err = json.Unmarshal(raw, &cuts); err != nil {
+ t.Fatal(err)
+ }
+ sawEdge := false
+ for _, cut := range cuts {
+ t.Run(fmt.Sprintf("%d-%s", cut.N, cut.Point), func(t *testing.T) {
+ offlineMatrixNames(t, cut.N)
+ tx := activationOffline{units: "/etc/systemd/system", state: fmt.Sprintf("%s/case-%03d", offlineRebootState, cut.N), systemctl: offlineRealSystemctl}
+ defer func() {
+ for _, name := range activationOfflineUnits() {
+ tx.systemctl(t.Context(), "disable", "--now", name)
+ os.Remove(filepath.Join(tx.units, name))
+ }
+ tx.systemctl(t.Context(), "daemon-reload")
+ os.RemoveAll(tx.state)
+ }()
+ load, active, err := tx.status(t.Context(), activationEdgeUnit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cut.Phase != "complete" && cut.Phase != "done" && (load != "masked" || active != "inactive") {
+ t.Fatalf("edge served before complete after %s: %s/%s", cut.Point, load, active)
+ }
+ if cut.Point == "replace-"+activationEdgeUnit {
+ sawEdge = true
+ if cut.Phase != "complete" {
+ t.Fatal("edge replacement preceded durable completion")
+ }
+ if load != "loaded" || active != "active" {
+ t.Fatalf("completed edge not booted: %s/%s", load, active)
+ }
+ }
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal("resume", err)
+ }
+ _, active, err = tx.status(t.Context(), activationEdgeUnit)
+ if err != nil || active != "active" {
+ t.Fatal("edge not restored", active, err)
+ }
+ if cut.Phase != "done" {
+ _, active, err = tx.status(t.Context(), activationTriggers[0])
+ if err != nil || active != "inactive" {
+ t.Fatal("enabled but inactive timer not reconciled", active, err)
+ }
+ }
+ t.Logf("actual reboot and restore passed: %s", cut.Point)
+ })
+ }
+ if !sawEdge {
+ t.Fatal("missing immediate edge-replacement reboot")
+ }
+ os.RemoveAll(offlineRebootState)
+}
+
+func TestRootActivationRestoreEnsurePayload(t *testing.T) {
+ if os.Getenv("CARLOS_ACTIVATION_ENSURE_PAYLOAD") != "1" {
+ t.Fatal("invoke only through the adoption fixture")
+ }
+ if _, err := os.Stat(offlineRebootState + "/fire"); os.IsNotExist(err) {
+ return
+ }
+ ensureSystemUnits("/etc", offlineRebootState+"/apply.lock", productionSysOps().systemctl, systemctlProbe, t.Logf)
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(offlineRebootState+"/fired", boot, 0600); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRootActivationRestoreSystemdAdoption(t *testing.T) {
+ tx := setupOfflineReal(t)
+ defer cleanupOfflineReal(t, tx)
+ tx.state = "/etc/carlos/claim-activation/offline"
+ if _, err := os.Lstat("/etc/carlos/claim-activation"); !os.IsNotExist(err) {
+ t.Fatal("pre-existing activation state", err)
+ }
+ defer os.RemoveAll("/etc/carlos/claim-activation")
+ for _, name := range []string{instanceControlSocket, claimControlUnit} {
+ if _, err := os.Lstat(filepath.Join(tx.units, name)); !os.IsNotExist(err) {
+ t.Fatal("pre-existing control unit", name, err)
+ }
+ }
+ if _, err := os.Lstat("/run/carlos-control"); !os.IsNotExist(err) {
+ t.Fatal("pre-existing control socket directory", err)
+ }
+ generationRunnerRegistryUser(t)
+ defer func() {
+ for _, name := range []string{instanceControlSocket, claimControlUnit} {
+ tx.systemctl(t.Context(), "disable", "--now", name)
+ os.Remove(filepath.Join(tx.units, name))
+ }
+ tx.systemctl(t.Context(), "daemon-reload")
+ os.RemoveAll("/run/carlos-control")
+ }()
+ bin, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw := "[Unit]\nDescription=real activation adoption repair\n[Service]\nType=oneshot\nEnvironment=CARLOS_ACTIVATION_ENSURE_PAYLOAD=1\nExecStart=" + bin + " -test.run=^TestRootActivationRestoreEnsurePayload$ -test.v\nRestrictAddressFamilies=AF_UNIX\nIPAddressDeny=any\n"
+ if err = os.WriteFile(filepath.Join(tx.units, "carlos-adopt.service"), []byte(raw), 0644); err != nil {
+ t.Fatal(err)
+ }
+ timer := "[Timer]\nOnBootSec=1ms\nOnUnitActiveSec=1h\n[Install]\nWantedBy=timers.target\n"
+ if err = os.WriteFile(filepath.Join(tx.units, "carlos-adopt.timer"), []byte(timer), 0644); err != nil {
+ t.Fatal(err)
+ }
+ for _, args := range [][]string{{"daemon-reload"}, {"restart", "carlos-adopt.timer"}, {"start", "carlos-adopt.service"}} {
+ if out, err := tx.systemctl(t.Context(), args...); err != nil {
+ t.Fatal(string(out), err)
+ }
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(offlineRebootState+"/fire", nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ stop := errors.New("interrupted after adoption activation")
+ tx.checkpoint = func(name string) error {
+ if name == "start-carlos-adopt.timer" {
+ return stop
+ }
+ return nil
+ }
+ if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
+ t.Fatal(err)
+ }
+ if err := runAdoptUnderRootRelease(nil, nil, nil, nil); !errors.Is(err, errActivation) {
+ t.Fatal("root adoption did not honor transaction", err)
+ }
+ for _, recover := range []bool{false, true} {
+ if err := (&systemApplier{etcDir: "/etc"}).Run(t.Context(), recover); !errors.Is(err, errActivation) {
+ t.Fatal("system apply did not honor transaction", recover, err)
+ }
+ }
+ path := filepath.Join(tx.units, "carlos-system-update.path")
+ before, err := os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if out, err := tx.systemctl(t.Context(), "start", "carlos-adopt.service"); err != nil {
+ t.Fatal(string(out), err)
+ }
+ if _, err := os.Stat(offlineRebootState + "/fired"); err != nil {
+ t.Fatal("real ensure did not run", err)
+ }
+ after, err := os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !os.SameFile(before, after) {
+ t.Fatal("adoption replaced the held path")
+ }
+ load, active, err := tx.status(t.Context(), "carlos-edge.service")
+ if err != nil || load != "masked" || active != "inactive" {
+ t.Fatal("edge escaped", load, active, err)
+ }
+ tx.checkpoint = nil
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ _, active, err = tx.status(t.Context(), "carlos-system-update.path")
+ if err != nil || active != "inactive" {
+ t.Fatal("inactive path was not restored", active, err)
+ }
+ if out, err := tx.systemctl(t.Context(), "start", "carlos-adopt.service"); err != nil {
+ t.Fatal(string(out), err)
+ }
+ after, err = os.Lstat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if os.SameFile(before, after) {
+ t.Fatal("ordinary adoption still blocked after done")
+ }
+ _, active, err = tx.status(t.Context(), "carlos-system-update.path")
+ if err != nil || active != "active" {
+ t.Fatal("ordinary adoption did not arm path", active, err)
+ }
+}
+
+func TestRootActivationAdoptionGate(t *testing.T) {
+ d := activationRootFixture(t)
+ etc := filepath.Join(d, "etc")
+ if err := os.Mkdir(etc, 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := activationAdoptionAllowed(etc); err != nil {
+ t.Fatal("absent transaction changed adoption", err)
+ }
+ if _, err := os.Lstat(filepath.Join(etc, "carlos")); !os.IsNotExist(err) {
+ t.Fatal("absent gate wrote state", err)
+ }
+ tx := activationOffline{units: filepath.Join(etc, "systemd", "system"), state: filepath.Join(etc, "carlos", "claim-activation", "offline")}
+ state, err := activationDir(tx.state, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer state.Close()
+ if err := activationAdoptionAllowed(etc); err == nil {
+ t.Fatal("missing journal permitted adoption")
+ }
+ r := activationOfflineRecord{V: 1, Units: tx.units}
+ for _, name := range activationOfflineUnits() {
+ r.Items = append(r.Items, activationOfflineItem{Name: name})
+ }
+ for _, phase := range []string{"prepared", "held", "seed-prepared", "restoring", "released", "complete", "done"} {
+ r.Phase = phase
+ if err := tx.save(state, r); err != nil {
+ t.Fatal(err)
+ }
+ err := activationAdoptionAllowed(etc)
+ if (err == nil) != (phase == "done") {
+ t.Fatal("gate phase", phase, err)
+ }
+ }
+ if err := os.Chmod(tx.state, 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := activationAdoptionAllowed(etc); err == nil {
+ t.Fatal("writable state permitted adoption")
+ }
+ if err := os.Chmod(tx.state, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(tx.state, tx.state+".original"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(tx.state+".original", tx.state); err != nil {
+ t.Fatal(err)
+ }
+ if err := activationAdoptionAllowed(etc); err == nil {
+ t.Fatal("symlink state permitted adoption")
+ }
+ if err := os.Remove(tx.state); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(tx.state+".original", tx.state); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(tx.state, "transaction.json"), []byte("{}"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := activationAdoptionAllowed(etc); err == nil {
+ t.Fatal("invalid journal permitted adoption")
+ }
+}
+
+func TestRootRebootActivationRestoreAdoptionPhase1(t *testing.T) {
+ tx := setupOfflineReal(t)
+ tx.state = "/etc/carlos/claim-activation/offline"
+ if _, err := os.Lstat("/etc/carlos/claim-activation"); !os.IsNotExist(err) {
+ t.Fatal("pre-existing activation state", err)
+ }
+ bin, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ unit := "[Unit]\nDescription=activation adoption reboot\n[Service]\nType=oneshot\nEnvironment=CARLOS_ACTIVATION_ENSURE_PAYLOAD=1\nExecStart=" + bin + " -test.run=^TestRootActivationRestoreEnsurePayload$ -test.v\nRestrictAddressFamilies=AF_UNIX\nIPAddressDeny=any\n"
+ if err := os.WriteFile(filepath.Join(tx.units, "carlos-adopt.service"), []byte(unit), 0644); err != nil {
+ t.Fatal(err)
+ }
+ timer := "[Timer]\nOnBootSec=1ms\nOnUnitActiveSec=1h\n[Install]\nWantedBy=timers.target\n"
+ if err := os.WriteFile(filepath.Join(tx.units, "carlos-adopt.timer"), []byte(timer), 0644); err != nil {
+ t.Fatal(err)
+ }
+ for _, args := range [][]string{{"daemon-reload"}, {"restart", "carlos-adopt.timer"}, {"start", "carlos-adopt.service"}} {
+ if out, err := tx.systemctl(t.Context(), args...); err != nil {
+ t.Fatal(string(out), err)
+ }
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(offlineRebootState+"/fire", nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ stop := errors.New("reboot during adoption restoration")
+ tx.checkpoint = func(name string) error {
+ if name == "start-carlos-adopt.timer" {
+ return stop
+ }
+ return nil
+ }
+ if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
+ t.Fatal(err)
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(offlineRebootState+"/boot", boot, 0600); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRootRebootActivationRestoreAdoptionPhase2(t *testing.T) {
+ tx := activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: offlineRealSystemctl}
+ defer cleanupOfflineReal(t, tx)
+ defer os.RemoveAll("/etc/carlos/claim-activation")
+ old, err := os.ReadFile(offlineRebootState + "/boot")
+ if err != nil {
+ t.Fatal(err)
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil || string(old) == string(boot) {
+ t.Fatal("no actual reboot", err)
+ }
+ fired, err := os.ReadFile(offlineRebootState + "/fired")
+ if err != nil || string(fired) != string(boot) {
+ t.Fatal("overdue boot timer did not invoke real ensureSystemUnits this boot", string(fired), err)
+ }
+ path := filepath.Join(tx.units, "carlos-system-update.path")
+ if !sameActivationFile(path, filepath.Join(tx.state, "carlos-system-update.path.original")) {
+ t.Fatal("boot adoption replaced held path")
+ }
+ if _, err := os.Stat(offlineRebootState + "/apply.lock"); !os.IsNotExist(err) {
+ t.Fatal("adoption crossed persistent gate", err)
+ }
+ load, active, err := tx.status(t.Context(), "carlos-edge.service")
+ if err != nil || load != "masked" || active != "inactive" {
+ t.Fatal("edge escaped on reboot", load, active, err)
+ }
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal("resume", err)
+ }
+ _, active, err = tx.status(t.Context(), "carlos-system-update.path")
+ if err != nil || active != "inactive" {
+ t.Fatal("recorded path state lost", active, err)
+ }
+ _, active, err = tx.status(t.Context(), "carlos-edge.service")
+ if err != nil || active != "active" {
+ t.Fatal("edge not restored", active, err)
+ }
+}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index b1c7db1b2..7623cfb28 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4340,6 +4340,9 @@ func runAdoptWithEnsureSystemUnits(args []string, buildClaims ClaimFactory, ensu
}
func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUnits func(), census func() (rootparticipation.Ledger, bool, error), updateProviders ...release.RootUpdateOpener) error {
+ if err := activationAdoptionAllowed("/etc"); err != nil {
+ return err
+ }
reapStagingWrites(release.OptDir(), dataDir(), "/etc", log.Printf)
if os.Geteuid() == 0 {
if err := repairOffcloudCredentials("/etc/carlos/credentials"); err != nil {
diff --git a/internal/host/systemapply.go b/internal/host/systemapply.go
index 9a959dadf..9bf6fb4f9 100644
--- a/internal/host/systemapply.go
+++ b/internal/host/systemapply.go
@@ -273,6 +273,9 @@ func (a *systemApplier) file(name string) string { return filepath.Join(a.rootDi
// journal is closed, and re-firing OnFailure= over a finished recovery
// would only start it again with nothing left to do.
func (a *systemApplier) Run(ctx context.Context, recover bool) (retErr error) {
+ if err := activationAdoptionAllowed(a.etcDir); err != nil {
+ return err
+ }
defer func() {
if errors.Is(retErr, context.DeadlineExceeded) {
if required, err := a.participationDisk().ParticipationRequired(context.WithoutCancel(ctx)); err == nil && required {
diff --git a/internal/host/systemunits.go b/internal/host/systemunits.go
index 2f97f0098..ce722e39c 100644
--- a/internal/host/systemunits.go
+++ b/internal/host/systemunits.go
@@ -40,6 +40,10 @@ var systemUnitFiles = []string{"carlos-system-update.path", "carlos-system-updat
// failure to report, and probe asks the two QUESTIONS, where non-zero is
// half the answer (see the block below).
func ensureSystemUnits(etcDir, lockPath string, systemctl, probe func(args ...string) (string, error), logf func(string, ...any)) {
+ if err := activationAdoptionAllowed(etcDir); err != nil {
+ logf("adopt: system units: %v", err)
+ return
+ }
if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil {
logf("adopt: system units: %v", err)
return
diff --git a/internal/provision/testdata/plan14-root-reads-activation-tools.tsv b/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
index ba436b9e8..3893d7b83 100644
--- a/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
+++ b/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
@@ -1,7 +1,7 @@
-# Activation-tools runs 1–3: proposals only; loaded after all existing overlays.
+# Activation-tools runs 1–4: proposals only; loaded after all existing overlays.
@coverage . efeeda08e816fc2c7e3aa451b06cce95ab5778af79682290cf98a60d8b9a841a
@coverage internal/claimactivation 625d82dce14bae92f619077ca92a8c9066a9446545dfcc96a6f818e89e20d4c2
-@coverage internal/host 6c413d6f9a104e24e0f333f9db7ea469e02b5ad02e57dd2bdfa4ad16babf0146
+@coverage internal/host d80147d3615919205f195df63aa3a81a0fb3780f354160aa78c76c75a5e85336
@coverage internal/hostfence 552570510fb62d2a8822a3f24fa76aeb1437c0a9516966dc5ce15ef58e3fb4c6
internal/claimactivation/runner.go Runner.Run authority F,G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=trusted operator phases bind expected signed roster revision, exact deployment and actors; verify each write; dry-run overlay never calls backing Put file:Read@310,file:Read@5986,file:Read@6318 683059aedf27656d4b49c88102089b2bb2f2ce7fcc108ff8545e52a86a0f8385
internal/claimactivation/runner.go checkedStore.Delete authority F,G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=trusted operator phases bind expected signed roster revision, exact deployment and actors; verify each write; dry-run overlay never calls backing Put 6bc9103a30cfd53cea59d611ccacb7dcc0ca0ea620b403497ef543f5bdaa0217
@@ -28,12 +28,12 @@ internal/host/activation_offline_linux.go activationOffline.dirs authority G,J4
internal/host/activation_offline_linux.go activationOffline.point authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 34292b297a697dbb9fd4c1b067900a7cdcdd0b41dba0d7c7dc9764f633424e43
internal/host/activation_offline_linux.go activationOffline.prepare authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call file:Lstat@947,file:Readlink@979 f3723f40487a5fe0fd43a8369be180b3238c808d9ce117612d744058a9fb0861
internal/host/activation_offline_linux.go activationOffline.preview authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call a092bb6fb96f64e406a266cef59a4516145a7f58935ff8d1bbc42a96a653aab1
-internal/host/activation_offline_linux.go activationOffline.read authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 2f782555914ab53ca36f53ed9bd881b9d8d55225c3828941b4733921d52d77a0
+internal/host/activation_offline_linux.go activationOffline.read authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 6ffaa998c2441b9a2d7889275b526bace11feb269df6856527b199a40cfa5bec
internal/host/activation_offline_linux.go activationOffline.replace authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 2a64167ac7053a5c73fe64aa9fea7e4f8db3b1409c7e34fa78c1b842328ccdbb
-internal/host/activation_offline_linux.go activationOffline.restore authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=released relinquishes ordinary service files only; retain trigger and edge ownership until done; reconcile inactive and active triggers and verify all before edge and completion; refuse replaced trigger cde53e59e9880e1b81209803fa5e556e817f43add08578d09070b971112e5852
+internal/host/activation_offline_linux.go activationOffline.restore authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=hold edge mask through durable complete; stop adoption triggers before service reconciliation; persistent journal excludes adoption and unit repair through done; restore triggers before complete and edge last; retain all original ownership until done f6b5f2fc9a6053e7ed39db2ac97ae5a2539b0247c7fadaf028d4edfb7f17fddc
internal/host/activation_offline_linux.go activationOffline.save authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 3082a1585eb287687e05756f495aae7de7e11aab6e96bbee920c50b4807ab9b7
internal/host/activation_offline_linux.go activationOffline.status authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 22467426d7d4cdb3d3f41dfab898f5a5ee49c34a58d4489ab53d6f04ab213587
-internal/host/activation_offline_linux.go activationOfflineUnits authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 946bc600ded790992d034479eb50b5363a3c9c50e3a3b47693e90318c357c8cc
+internal/host/activation_offline_linux.go activationOfflineUnits authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call 372c65c44288c766e9b2aa0ece8cbc55c95fb1ccae7e9bae80a5bfcda2550b4b
internal/host/activation_offline_linux.go activationUnitSnapshot authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call file:Lstat@135,file:ReadFile@819,file:Readlink@539 bcb4d45f273e849f03715cfb3a6b49b25b3e128d55e88b15a9528175e433ad04
internal/host/activation_offline_linux.go sameActivationFile authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=explicit root operator command only; protected ancestry, exact inputs, retained mask ownership and durable readback; no automatic claims-off call file:Lstat@107,file:Lstat@53 72cf415a823352f1fcf5aba8452df074896aacc851da3dfea9a635b37c44669b
internal/hostfence/activation_credentials.go ActivationCredentialReceipt authority F,J4 rev3=PROPOSED-activation-tools;identity=root fence service and explicit installer uid 0;basis=dedicated root fence credentials retain 0400 compatibility; 0600 needs a matching protected installation receipt; absent explicit installation keeps prior refusal 63ffd51457e632687bc44f776fd5c0335748a899e623ff67a0dea31be4ffa7f2
@@ -60,3 +60,9 @@ internal/host/activation_seed_linux.go activationOffline.seedRemask authority G,
internal/host/activation_seed_linux.go const activationSeedGuardOriginal authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot de4e081ef3584a30477e20d3d30bcc0d1f3582607be533921c3896d6548440f2
internal/host/activation_seed_linux.go const activationSeedGuardReplacement authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot d5d0cacb84f15b9d5098e05f41a7981852dc53add1c20b946a1a870ef3dc2736
internal/host/activation_seed_linux.go activationSeedReadAt authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=protected directory descriptor; nofollow nonblocking regular-file read; root ownership and bounded bytes; retained hardlinks permitted only in seed configuration inspection file:Fstat@368,file:Openat@148,file:ReadAll@545 0fecdfd3967cc9db0f5a5f2cb81a8eb4d440ffb145ab9ef604c77f654d74e94e
+internal/host/activation_adoption_linux.go activationAdoptionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. file:Lstat@139,file:Stat@359 0d69ed444fc4f8f371ce13993031d209e6b872c691134251012d497a33febd66
+internal/host/activation_adoption_other.go activationAdoptionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. 9a70a93e601ea57fe2ce1780cc6cf878ea3cde656770fe8c400d346fbdbca361
+internal/host/activation_offline_linux.go var activationEdgeUnit authority G,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. 42c79dbb523cf08c19f23d2e4f1754902524276b86f58f0d3d5bfb6117d3fcc6
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=no census selects legacy proof/history and update effects; proof observation cannot fetch signed recovery manifests on that path; participating adoption retains root release exclusion and independent generation pins whole:Route@11179,whole:Route@18368,whole:Route@18404,whole:Route@8481 2f755c7ed82fcac622a118d1e4ad0b5d746de2b97e0456e125114e12351cd0c8
+internal/host/systemunits.go ensureSystemUnits authority F,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. root-input;kept=F;scope=protected-decision-only file:OpenFile@375,file:ReadFile@3018,file:ReadFile@3151,file:ReadFile@4065,file:Stat@2927 25aa3ae39d4ccbb5aa041caceb98fe2512c4a0ccfe1a1d34776750d46eff3a06
+internal/host/systemapply.go systemApplier.Run authority D,F,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. root-input;kept=D,F;scope=protected-decision-only PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. file:OpenFile@560 1a0ba9c44022eb6e624b38e99cc356bd1f42701ef48c187aa18f80e2d3312884