carlos / platform Public

Claim activation tools, run 9: narrowed admission, exact restore (not for merge)

offline-begin refuses unsupported baselines (failed, transitioning, masked-but-active, irregular edge) before recording or stopping anything; restore reproduces exact recorded state for every unit including the edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 28814d7bbe6793ad1fa3e7d484b3be5846c8637d parent 918d0e1
6 files changed, +437 −84
  • internal/host/activation_baseline_rootrunner_linux_test.go +274 −0
  • internal/host/activation_invariants_rootrunner_linux_test.go +2 −2
  • internal/host/activation_matrix_rootrunner_linux_test.go +1 −1
  • internal/host/activation_offline_linux.go +135 −70
  • internal/host/activation_production_reboot_rootrunner_linux_test.go +14 −4
  • internal/provision/testdata/plan14-root-reads-activation-tools.tsv +11 −7

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	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	316.535s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.777s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.425s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.706s
ok  	github.com/carlosframework/platform/internal/activator	250.065s
ok  	github.com/carlosframework/platform/internal/agentsock	0.024s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.024s
ok  	github.com/carlosframework/platform/internal/appconfig	2.487s
ok  	github.com/carlosframework/platform/internal/apperr	0.088s
ok  	github.com/carlosframework/platform/internal/applog	0.147s
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.548s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.025s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.709s
ok  	github.com/carlosframework/platform/internal/changelog	0.316s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.025s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	393.936s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.846s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.013s
ok  	github.com/carlosframework/platform/internal/dbverify	0.010s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.407s
ok  	github.com/carlosframework/platform/internal/doorkey	0.010s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.684s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.009s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.145s
ok  	github.com/carlosframework/platform/internal/gate	1.663s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.021s
ok  	github.com/carlosframework/platform/internal/geoip	0.036s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.175s
ok  	github.com/carlosframework/platform/internal/host	174.918s
ok  	github.com/carlosframework/platform/internal/hostactors	0.322s
ok  	github.com/carlosframework/platform/internal/hostbox	2.683s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.506s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.818s
ok  	github.com/carlosframework/platform/internal/hostfence	0.032s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.421s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	1.031s
ok  	github.com/carlosframework/platform/internal/hostops	1.609s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.458s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.178s
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.223s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.758s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.020s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	1.568s
ok  	github.com/carlosframework/platform/internal/lease	9.797s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.019s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.171s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.573s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.080s
ok  	github.com/carlosframework/platform/internal/metrics	0.007s
ok  	github.com/carlosframework/platform/internal/moves	21.036s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.786s
ok  	github.com/carlosframework/platform/internal/objstore	0.216s
ok  	github.com/carlosframework/platform/internal/ops	0.173s
ok  	github.com/carlosframework/platform/internal/park	0.239s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	22.898s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.514s
ok  	github.com/carlosframework/platform/internal/relay	7.238s
ok  	github.com/carlosframework/platform/internal/release	22.220s
ok  	github.com/carlosframework/platform/internal/rollout	3.690s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
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.575s
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.540s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.742s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.154s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.439s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.009s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.551s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
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: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/17 00:32:11 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF29CBB6F8E970, 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.022s
minio contract: PASS
diff --git a/internal/host/activation_baseline_rootrunner_linux_test.go b/internal/host/activation_baseline_rootrunner_linux_test.go
new file mode 100644
index 000000000..ea4f5092c
--- /dev/null
+++ b/internal/host/activation_baseline_rootrunner_linux_test.go
@@ -0,0 +1,274 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+func activationAssertAdmissionRefused(t *testing.T, tx activationOffline, names ...string) {
+ t.Helper()
+ before := map[string]activationOfflineItem{}
+ for _, name := range activationOfflineUnits() {
+ item, err := activationUnitSnapshot(filepath.Join(tx.units, name), name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ before[name] = item
+ }
+ real := tx.systemctl
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] != "show" {
+ t.Fatalf("admission refusal made systemctl mutation: %v", args)
+ }
+ return real(ctx, args...)
+ }
+ for _, dry := range []bool{true, false} {
+ err := tx.begin(t.Context(), dry)
+ if err == nil {
+ t.Fatalf("unsupported baseline admitted (dry=%t)", dry)
+ }
+ for _, name := range names {
+ if !strings.Contains(err.Error(), name) {
+ t.Errorf("missing unit/state %q in refusal: %v", name, err)
+ }
+ }
+ t.Logf("refused dry=%t: %v", dry, err)
+ }
+ entries, err := os.ReadDir(tx.state)
+ if err != nil && !os.IsNotExist(err) {
+ t.Fatal(err)
+ }
+ for _, entry := range entries {
+ if entry.Name() != ".claims-activation.lock" {
+ t.Errorf("refusal recorded artifact: %s", entry.Name())
+ }
+ }
+ for name, item := range before {
+ got, err := activationUnitSnapshot(filepath.Join(tx.units, name), name)
+ if err != nil || got != item {
+ t.Errorf("refusal changed %s: %v", name, err)
+ }
+ }
+}
+
+func TestRootActivationBaselineAdmission(t *testing.T) {
+ for _, name := range []string{"carlos-adopt.timer", "carlos-config-sync.service", activationEdgeUnit} {
+ for _, active := range []string{"failed", "activating", "deactivating", "reloading"} {
+ t.Run(name+"/"+active, func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ real := tx.systemctl
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] == "show" && args[1] == name {
+ return []byte("LoadState=loaded\nActiveState=" + active + "\n"), nil
+ }
+ return real(ctx, args...)
+ }
+ activationAssertAdmissionRefused(t, tx, name, "active="+active)
+ })
+ }
+ for _, active := range []string{"active", "inactive"} {
+ if active == "inactive" && name != activationEdgeUnit {
+ continue
+ }
+ t.Run(name+"/masked/"+active, func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ path := filepath.Join(tx.units, name)
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("/dev/null", path); err != nil {
+ t.Fatal(err)
+ }
+ real := tx.systemctl
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] == "show" && args[1] == name {
+ return []byte("LoadState=masked\nActiveState=" + active + "\n"), nil
+ }
+ return real(ctx, args...)
+ }
+ activationAssertAdmissionRefused(t, tx, name, "load=masked", "active="+active)
+ })
+ }
+ }
+ for _, load := range []string{"not-found", "error", "bad-setting", "masked"} {
+ t.Run("load/"+load, func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ real := tx.systemctl
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] == "show" && args[1] == activationEdgeUnit {
+ return []byte("LoadState=" + load + "\nActiveState=inactive\n"), nil
+ }
+ return real(ctx, args...)
+ }
+ activationAssertAdmissionRefused(t, tx, activationEdgeUnit, "load="+load, "active=inactive")
+ })
+ }
+
+ t.Run("all-unsupported-units", func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ real := tx.systemctl
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if args[0] == "show" && (args[1] == activationEdgeUnit || args[1] == "carlos-adopt.timer") {
+ return []byte("LoadState=loaded\nActiveState=failed\n"), nil
+ }
+ return real(ctx, args...)
+ }
+ activationAssertAdmissionRefused(t, tx, activationEdgeUnit, "carlos-adopt.timer", "active=failed")
+ })
+}
+
+func TestRootActivationProductionBaselineCombinations(t *testing.T) {
+ for _, edge := range []bool{false, true} {
+ for _, timer := range []bool{false, true} {
+ t.Run(fmt.Sprintf("edge=%t/timer=%t", edge, timer), func(t *testing.T) {
+ tx := setupActivationProduction(t)
+ defer cleanupActivationProduction(t, tx)
+ if !edge {
+ servingRunnerSystemctl(t, "stop", activationEdgeUnit)
+ }
+ if timer {
+ servingRunnerSystemctl(t, "start", "carlos-adopt.timer")
+ servingRunnerSystemctl(t, "stop", "carlos-adopt.service")
+ servingRunnerSystemctl(t, "reset-failed", "carlos-adopt.service")
+ }
+ // Inactive operator masks are supported on both a trigger and a service.
+ for _, name := range []string{"carlos-stage.path", "carlos-stage.service"} {
+ if err := os.Remove(filepath.Join(tx.units, name)); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("/dev/null", filepath.Join(tx.units, name)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ servingRunnerSystemctl(t, "daemon-reload")
+ if err := tx.begin(t.Context(), true); err != nil {
+ t.Fatal("dry admission", err)
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal("admission", err)
+ }
+ r := offlineRecord(t, tx)
+ if r.Items[0].Active != timer || r.Items[len(r.Items)-1].Active != edge {
+ t.Fatal("wrong recorded baseline")
+ }
+ // A live edge after durable closure must still be stopped when the
+ // admitted edge was inactive. Keep the same recorded unit inode.
+ if !edge {
+ tx.checkpoint = func(point string) error {
+ if point == "edge-reload" {
+ servingRunnerSystemctl(t, "start", activationEdgeUnit)
+ }
+ return nil
+ }
+ }
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal("restore", err)
+ }
+ r = offlineRecord(t, tx)
+ if r.Phase != "done" || r.EdgePending || r.TriggersPending {
+ t.Fatal("unfinished", r.Phase)
+ }
+ for _, item := range r.Items {
+ load, active, err := tx.status(t.Context(), item.Name)
+ wantLoad, wantActive := activationRecordedState(item)
+ if err != nil || load != wantLoad || active != wantActive {
+ t.Errorf("%s: %s/%s want %s/%s: %v", item.Name, load, active, wantLoad, wantActive, err)
+ }
+ }
+ t.Logf("production admission-to-completion edge=%t timer=%t", edge, timer)
+ })
+ }
+ }
+}
+
+func TestRootActivationProductionUnsupportedBaselines(t *testing.T) {
+ for _, scenario := range []string{"failed-edge", "masked-active-trigger", "activating-unit"} {
+ t.Run(scenario, func(t *testing.T) {
+ tx := setupActivationProduction(t)
+ defer cleanupActivationProduction(t, tx)
+ name, load, active := activationEdgeUnit, "loaded", "failed"
+ switch scenario {
+ case "failed-edge":
+ activationProductionWrite(t, filepath.Join(tx.units, activationEdgeUnit+".d/95-failure.conf"), []byte("[Service]\nRestart=no\n"), 0644)
+ servingRunnerSystemctl(t, "daemon-reload")
+ servingRunnerSystemctl(t, "kill", "--signal=SIGKILL", activationEdgeUnit)
+ case "masked-active-trigger":
+ name, load, active = "carlos-stage.path", "masked", "active"
+ servingRunnerSystemctl(t, "start", name)
+ if err := os.Remove(filepath.Join(tx.units, name)); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("/dev/null", filepath.Join(tx.units, name)); err != nil {
+ t.Fatal(err)
+ }
+ servingRunnerSystemctl(t, "daemon-reload")
+ case "activating-unit":
+ name, active = "carlos-config-sync.service", "activating"
+ activationProductionWrite(t, filepath.Join(tx.units, name+".d/95-delay.conf"), []byte("[Service]\nExecStartPre=/bin/sleep 60\n"), 0644)
+ servingRunnerSystemctl(t, "daemon-reload")
+ servingRunnerSystemctl(t, "start", "--no-block", name)
+ }
+ deadline := time.Now().Add(5 * time.Second)
+ for {
+ gotLoad, gotActive, _ := tx.status(t.Context(), name)
+ if gotLoad == load && gotActive == active {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("fixture %s=%s/%s want %s/%s", name, gotLoad, gotActive, load, active)
+ }
+ time.Sleep(20 * time.Millisecond)
+ }
+ activationAssertAdmissionRefused(t, tx, name, "load="+load, "active="+active)
+ })
+ }
+}
+
+func TestRootActivationExactEdgeVerification(t *testing.T) {
+ for _, want := range []string{"active", "inactive"} {
+ t.Run(want, func(t *testing.T) {
+ tx, _ := offlineFixture(t)
+ real := tx.systemctl
+ active := want
+ tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
+ if len(args) > 1 && args[1] == activationEdgeUnit {
+ switch args[0] {
+ case "start":
+ active = "active"
+ case "stop":
+ active = "inactive"
+ case "show":
+ raw, err := real(ctx, args...)
+ return []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState="+active)), err
+ }
+ }
+ return real(ctx, args...)
+ }
+ if err := tx.begin(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ if err := tx.restore(t.Context(), false); err != nil {
+ t.Fatal(err)
+ }
+ r := offlineRecord(t, tx)
+ if diff := tx.closeDifferences(t.Context(), r); len(diff) != 0 {
+ t.Fatal("exact restored state rejected", diff)
+ }
+ active = "active"
+ if want == "active" {
+ active = "inactive"
+ }
+ diff := strings.Join(tx.closeDifferences(t.Context(), r), "\n")
+ if !strings.Contains(diff, activationEdgeUnit+": load=loaded active="+active+"; want load=loaded active="+want) {
+ t.Fatal("edge target substituted", diff)
+ }
+ })
+ }
+}
diff --git a/internal/host/activation_invariants_rootrunner_linux_test.go b/internal/host/activation_invariants_rootrunner_linux_test.go
index 1a417d909..e97388655 100644
--- a/internal/host/activation_invariants_rootrunner_linux_test.go
+++ b/internal/host/activation_invariants_rootrunner_linux_test.go
@@ -156,7 +156,7 @@ func TestRootActivationUpdateInProgress(t *testing.T) {
}
return base(ctx, args...)
}
- if err := tx.begin(t.Context(), false); err == nil || !strings.Contains(err.Error(), "settle the update first") {
+ if err := tx.begin(t.Context(), false); err == nil || !strings.Contains(err.Error(), "active=activating") {
t.Fatal("activating update admitted", err)
}
}
@@ -199,7 +199,7 @@ func TestRootActivationProductionCloseRefusesInode(t *testing.T) {
}
func TestRootActivationRecordedStates(t *testing.T) {
- for _, index := range []int{0, len(activationTriggers)} {
+ for _, index := range []int{0, len(activationTriggers), len(activationOfflineUnits()) - 1} {
for _, item := range []activationOfflineItem{
{LoadState: "loaded", ActiveState: "failed"},
{LoadState: "masked", ActiveState: "active", Masked: true, Active: true},
diff --git a/internal/host/activation_matrix_rootrunner_linux_test.go b/internal/host/activation_matrix_rootrunner_linux_test.go
index 4889241bb..796cc0450 100644
--- a/internal/host/activation_matrix_rootrunner_linux_test.go
+++ b/internal/host/activation_matrix_rootrunner_linux_test.go
@@ -116,7 +116,7 @@ func activationMatrixCheck(t *testing.T, tx activationOffline, point string) {
}
}
if point == "journal-done" && r.EdgePending {
- if differences := tx.closeDifferences(t.Context(), r); len(differences) != 0 {
+ if differences := tx.heldDifferences(t.Context(), r); len(differences) != 0 {
t.Fatal("closed on mismatch", differences)
}
}
diff --git a/internal/host/activation_offline_linux.go b/internal/host/activation_offline_linux.go
index e3f628327..2acef2f38 100644
--- a/internal/host/activation_offline_linux.go
+++ b/internal/host/activation_offline_linux.go
@@ -195,22 +195,54 @@ func (t activationOffline) read(state *os.File) (activationOfflineRecord, error)
}
return r, nil
}
+func activationSupportedBaseline(item activationOfflineItem) bool {
+ load, active := activationRecordedState(item)
+ if active != "active" && active != "inactive" {
+ return false
+ }
+ if item.Masked {
+ return item.Name != activationEdgeUnit && load == "masked" && active == "inactive"
+ }
+ return load == "loaded"
+}
+
+func (t activationOffline) baseline(ctx context.Context) ([]activationOfflineItem, error) {
+ units, err := activationDir(t.units, false)
+ if err != nil {
+ return nil, err
+ }
+ defer units.Close()
+ var items []activationOfflineItem
+ var unsupported []string
+ for _, name := range activationOfflineUnits() {
+ item, fileErr := activationUnitSnapshot(filepath.Join(t.units, name), name)
+ load, active, stateErr := t.status(ctx, name)
+ item.LoadState, item.ActiveState, item.Active = load, active, active == "active"
+ if fileErr != nil || stateErr != nil || !activationSupportedBaseline(item) {
+ detail := fmt.Sprintf("%s: load=%s active=%s file_mask=%t", name, load, active, item.Masked)
+ if err := errors.Join(fileErr, stateErr); err != nil {
+ detail += ": " + err.Error()
+ }
+ unsupported = append(unsupported, detail)
+ }
+ items = append(items, item)
+ }
+ if len(unsupported) != 0 {
+ return nil, fmt.Errorf("claim activation: offline-begin requires supported unit states; no units were recorded or stopped:\n%s", strings.Join(unsupported, "\n"))
+ }
+ return items, nil
+}
+
func (t activationOffline) prepare(ctx context.Context, units, state *os.File) (activationOfflineRecord, error) {
r := activationOfflineRecord{V: 1, Units: t.units, Phase: "prepared"}
- for _, name := range activationOfflineUnits() {
+ items, err := t.baseline(ctx)
+ if err != nil {
+ return r, err
+ }
+ for _, item := range items {
+ name := item.Name
path := filepath.Join(t.units, name)
backup := filepath.Join(t.state, name+".original")
- item, err := activationUnitSnapshot(path, name)
- if err != nil {
- return r, err
- }
- load, active, err := t.status(ctx, name)
- if err != nil {
- return r, err
- }
- item.Active = active == "active"
- item.LoadState = load
- item.ActiveState = active
if err = os.Link(path, backup); err != nil && !errors.Is(err, os.ErrExist) {
return r, err
}
@@ -308,6 +340,25 @@ func (t activationOffline) replace(units, state *os.File, item activationOffline
return nil
}
func (t activationOffline) begin(ctx context.Context, dry bool) error {
+ // Admission is read-only and covers the whole set before any snapshots.
+ // Resumes use the durable baseline: our own masks are not a new baseline.
+ fresh := true
+ if state, err := activationDir(t.state, false); err == nil {
+ _, err = t.read(state)
+ state.Close()
+ if err == nil {
+ fresh = false
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ if fresh {
+ if _, err := t.baseline(ctx); err != nil {
+ return err
+ }
+ }
release, err := t.settled(ctx, dry)
if err != nil {
return err
@@ -511,25 +562,20 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
return err
}
}
- // Triggers stay inactive until durable closure. Starting an overdue timer
- // here can run a service after that service has already been verified.
+ // Restore-issued trigger starts wait for durable closure. An enabled
+ // timer can start on reboot once its file is restored; resume stops it.
+ // Edge and active triggers have separate exact-state checks after done.
var differences []string
const rounds = 3
for round := 0; round < rounds; round++ {
differences = nil
for i, item := range r.Items {
- want := item
- if i < len(activationTriggers) && item.Active && !item.Masked {
- want.Active = false
- want.ActiveState = "inactive"
+ if item.Name == activationEdgeUnit || i < len(activationTriggers) && item.Active {
+ err = t.hold(ctx, item.Name)
+ } else {
+ err = t.reconcile(ctx, item)
}
- if item.Name == activationEdgeUnit {
- want.Masked = true
- want.LoadState = "masked"
- want.Active = false
- want.ActiveState = "inactive"
- }
- if err = t.reconcile(ctx, want); err != nil {
+ if err != nil {
var interrupted activationInterruption
if errors.As(err, &interrupted) {
return err
@@ -540,7 +586,7 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
if err = t.point("verify-close"); err != nil {
return err
}
- differences = append(differences, t.closeDifferences(ctx, r)...)
+ differences = append(differences, t.heldDifferences(ctx, r)...)
if len(differences) == 0 {
break
}
@@ -572,17 +618,12 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
}
release()
release = func() {}
- // Inactive state was established before done. Never stop an edge that
- // ordinary boot or recovery has since started. Only active startup is owed.
- if r.EdgePending && edgeOwned && edge.Active && !edge.Masked && t.checkItem(edge) == nil {
+ // The pending edge still owes its exact recorded state, including inactive.
+ // A later writer's replacement is outside this transaction's authority.
+ if r.EdgePending && edgeOwned && t.checkItem(edge) == nil {
if err = t.reconcile(ctx, edge); err != nil {
return err
}
- load, active, e := t.status(ctx, edge.Name)
- wantLoad, wantActive := activationRecordedState(edge)
- if e != nil || load != wantLoad || active != wantActive {
- return fmt.Errorf("%s: load=%s active=%s; want load=%s active=%s", edge.Name, load, active, wantLoad, wantActive)
- }
}
r.EdgePending = false
if err = t.save(state, r); err != nil {
@@ -595,11 +636,8 @@ func (t activationOffline) restore(ctx context.Context, dry bool) error {
}
release = nextRelease
for _, item := range r.Items[:len(activationTriggers)] {
- if item.Active && !item.Masked && t.checkItem(item) == nil {
- if _, err = t.systemctl(ctx, "start", item.Name); err != nil {
- return err
- }
- if err = t.point("start-" + item.Name); err != nil {
+ if t.checkItem(item) == nil {
+ if err = t.reconcile(ctx, item); err != nil {
return err
}
}
@@ -664,61 +702,88 @@ func (t activationOffline) preview(ctx context.Context) ([]activationOfflinePrev
return out, nil
}
-func (t activationOffline) reconcile(ctx context.Context, item activationOfflineItem) error {
- if item.ActiveState == "failed" {
- load, active, err := t.status(ctx, item.Name)
- wantLoad, wantActive := activationRecordedState(item)
- if err != nil || load != wantLoad || active != wantActive {
- return fmt.Errorf("%s: load=%s active=%s; want load=%s active=%s", item.Name, load, active, wantLoad, wantActive)
- }
- return nil
- }
- verb := "stop"
- if item.Active && !item.Masked {
- verb = "start"
- }
- if _, err := t.systemctl(ctx, verb, item.Name); err != nil {
- return fmt.Errorf("%s: %w", item.Name, err)
+func (t activationOffline) hold(ctx context.Context, name string) error {
+ if _, err := t.systemctl(ctx, "stop", name); err != nil {
+ return err
}
- if err := t.point(verb + "-" + item.Name); err != nil {
+ if err := t.point("stop-" + name); err != nil {
return err
}
- _, active, err := t.status(ctx, item.Name)
+ _, active, err := t.status(ctx, name)
if err != nil {
- return fmt.Errorf("%s: %w", item.Name, err)
+ return err
+ }
+ if active == "failed" {
+ if _, err := t.systemctl(ctx, "reset-failed", name); err != nil {
+ return err
+ }
+ return t.point("reset-failed-" + name)
+ }
+ return nil
+}
+
+func (t activationOffline) reconcile(ctx context.Context, item activationOfflineItem) error {
+ if !activationSupportedBaseline(item) {
+ return fmt.Errorf("%s: %w", item.Name, errActivation)
}
- if verb == "stop" && active == "failed" {
- if _, err := t.systemctl(ctx, "reset-failed", item.Name); err != nil {
+ _, wantActive := activationRecordedState(item)
+ if wantActive == "active" {
+ if _, err := t.systemctl(ctx, "start", item.Name); err != nil {
return fmt.Errorf("%s: %w", item.Name, err)
}
- if err := t.point("reset-failed-" + item.Name); err != nil {
+ if err := t.point("start-" + item.Name); err != nil {
return err
}
+ } else if err := t.hold(ctx, item.Name); err != nil {
+ return err
+ }
+ load, active, err := t.status(ctx, item.Name)
+ wantLoad, wantActive := activationRecordedState(item)
+ if err != nil || load != wantLoad || active != wantActive {
+ return fmt.Errorf("%s: load=%s active=%s; want load=%s active=%s", item.Name, load, active, wantLoad, wantActive)
}
return nil
}
-func (t activationOffline) closeDifferences(ctx context.Context, r activationOfflineRecord) []string {
+// The pre-close barrier verifies services against their recorded state, and
+// checks temporary holds separately. It does not certify the edge or deferred
+// triggers as restored; their pending flags survive until exact reconciliation.
+func (t activationOffline) heldDifferences(ctx context.Context, r activationOfflineRecord) []string {
+ stable := r
+ stable.Items = nil
var differences []string
for i, item := range r.Items {
- wantLoad, wantActive := activationRecordedState(item)
- if i < len(activationTriggers) && item.Active && !item.Masked {
- wantActive = "inactive"
+ if item.Name != activationEdgeUnit && !(i < len(activationTriggers) && item.Active) {
+ stable.Items = append(stable.Items, item)
+ continue
}
+ wantLoad, _ := activationRecordedState(item)
+ suffix := ".original"
if item.Name == activationEdgeUnit {
- wantLoad, wantActive = "masked", "inactive"
+ wantLoad, suffix = "masked", ".mask"
+ }
+ load, active, err := t.status(ctx, item.Name)
+ if err != nil || load != wantLoad || active != "inactive" {
+ differences = append(differences, fmt.Sprintf("%s: load=%s active=%s; want load=%s active=%s", item.Name, load, active, wantLoad, "inactive"))
+ }
+ if err := t.checkItem(item); err != nil || !sameActivationFile(filepath.Join(t.units, item.Name), filepath.Join(t.state, item.Name+suffix)) {
+ differences = append(differences, fmt.Sprintf("%s: recorded file inode, content, or metadata changed", item.Name))
}
+ }
+ return append(differences, t.closeDifferences(ctx, stable)...)
+}
+
+func (t activationOffline) closeDifferences(ctx context.Context, r activationOfflineRecord) []string {
+ var differences []string
+ for _, item := range r.Items {
+ wantLoad, wantActive := activationRecordedState(item)
load, active, err := t.status(ctx, item.Name)
if err != nil {
differences = append(differences, fmt.Sprintf("%s: load=%s active=%s; want load=%s active=%s: %v", item.Name, load, active, wantLoad, wantActive, err))
} else if load != wantLoad || active != wantActive {
differences = append(differences, fmt.Sprintf("%s: load=%s active=%s; want load=%s active=%s", item.Name, load, active, wantLoad, wantActive))
}
- suffix := ".original"
- if item.Name == activationEdgeUnit && !item.Masked {
- suffix = ".mask"
- }
- if err := t.checkItem(item); err != nil || !sameActivationFile(filepath.Join(t.units, item.Name), filepath.Join(t.state, item.Name+suffix)) {
+ if err := t.checkItem(item); err != nil || !sameActivationFile(filepath.Join(t.units, item.Name), filepath.Join(t.state, item.Name+".original")) {
differences = append(differences, fmt.Sprintf("%s: recorded file inode, content, or metadata changed", item.Name))
}
}
diff --git a/internal/host/activation_production_reboot_rootrunner_linux_test.go b/internal/host/activation_production_reboot_rootrunner_linux_test.go
index ee913fc60..215d4debc 100644
--- a/internal/host/activation_production_reboot_rootrunner_linux_test.go
+++ b/internal/host/activation_production_reboot_rootrunner_linux_test.go
@@ -96,6 +96,12 @@ func activationProductionRebootPhase2(t *testing.T, phase string) {
}
}
if phase == "complete" {
+ // The enabled timer has its original file at this pre-close reboot.
+ load, active, err := tx.status(t.Context(), "carlos-adopt.timer")
+ if err != nil || load != "loaded" || active != "active" {
+ t.Fatal("enabled timer did not start before close on reboot", load, active, err)
+ }
+ t.Log("enabled timer started on reboot before close; resume must reconcile it")
// Real services, started after the actual reboot with deliberately missing
// configuration. Every unmasked service must be reconciled, including failed.
if err := os.Rename("/etc/carlos/host.env", "/etc/carlos/host.env.saved"); err != nil {
@@ -110,12 +116,16 @@ func activationProductionRebootPhase2(t *testing.T, phase string) {
servingRunnerSystemctl(t, "start", "carlos-adopt.timer")
}
if phase == "done" {
- // Closure releases ordinary boot behavior. The pending edge step must not
- // restore old service files or roll an enabled timer back to its old state.
+ // Closure releases ordinary service writers. Pending triggers and the
+ // edge still owe their exact state while their files remain owned.
original := tx.systemctl
tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if (args[0] == "start" || args[0] == "stop") && args[1] != activationEdgeUnit {
- t.Fatalf("closed transaction changed %s", args[1])
+ if args[0] == "start" || args[0] == "stop" {
+ for _, name := range rootMigrationOfflineUnits {
+ if args[1] == name {
+ t.Fatalf("closed transaction changed %s", args[1])
+ }
+ }
}
return original(ctx, args...)
}
diff --git a/internal/provision/testdata/plan14-root-reads-activation-tools.tsv b/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
index d35e657a2..f7b831651 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–6: proposals only; loaded after all existing overlays.
+# Activation-tools runs 1–9: proposals only; loaded after all existing overlays.
@coverage . efeeda08e816fc2c7e3aa451b06cce95ab5778af79682290cf98a60d8b9a841a
@coverage internal/claimactivation 625d82dce14bae92f619077ca92a8c9066a9446545dfcc96a6f818e89e20d4c2
-@coverage internal/host 0b153b028de260c2050dfd4087bf4ba16eedb7dfc685797d24ed8edad86d9c9b
+@coverage internal/host d620d371d9c5ae6ab5301881099f9f7bd394152653bbdf20c6513df2bd4ef68d
@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
@@ -22,15 +22,15 @@ internal/host/activation_install_linux.go installActivationConsole authority G,J
internal/host/activation_install_linux.go installActivationControl 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:ReadFile@616 16e3c76c1d1d06fc429725cfeaee21b17d6a37620da287b40482b12e2f6b20a5
internal/host/activation_install_linux.go installActivationEvidenceKey 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 74f311fcb31aec95f5760ae9781186cc44a6f650858349c379e82f3a1fc6fd10
internal/host/activation_install_linux.go installActivationFenceCredentials 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 9e18de6df5fb9d3e6898967c9ed91e72d19faf5fd6bff7331ee654d76dfe7075
-internal/host/activation_offline_linux.go activationOffline.begin authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 5a266bc0738e51f4813bf83c54ef7e0fb7d29fbf73af8e2c86ffa10e16049843
+internal/host/activation_offline_linux.go activationOffline.begin authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission before recording; exact recorded state verification; temporary holds checked separately; existing updater exclusion and file ownership retained e1a8254f6cd071d09fc7e1ca4bdf4b2db35ae625c1dbeb1ef3c0599234ffa4e5
internal/host/activation_offline_linux.go activationOffline.checkItem authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 55e4f594ebd4666d8b5eff5eb995a163a6f9ef3838b1d6db5f705a90b93a16e8
internal/host/activation_offline_linux.go activationOffline.dirs authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior file:Stat@384 cdbba773717e890be772460b0f8235b4f7f0e158a11977740a2c963aa3a1c923
internal/host/activation_offline_linux.go activationOffline.point authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior c28de19acebdc98b1b836151fbba47103ccb431f40d79984fee55a0b921ad0e0
-internal/host/activation_offline_linux.go activationOffline.prepare authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior file:Lstat@947,file:Readlink@979 f3723f40487a5fe0fd43a8369be180b3238c808d9ce117612d744058a9fb0861
+internal/host/activation_offline_linux.go activationOffline.prepare authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission before recording; exact recorded state verification; temporary holds checked separately; existing updater exclusion and file ownership retained file:Lstat@757,file:Readlink@789 88a9d196707cf63be2d1802a0de9efaac9fe9c0bea756340be878873b124c6c6
internal/host/activation_offline_linux.go activationOffline.preview authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior a092bb6fb96f64e406a266cef59a4516145a7f58935ff8d1bbc42a96a653aab1
internal/host/activation_offline_linux.go activationOffline.read authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 6ffaa998c2441b9a2d7889275b526bace11feb269df6856527b199a40cfa5bec
internal/host/activation_offline_linux.go activationOffline.replace authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 2a64167ac7053a5c73fe64aa9fea7e4f8db3b1409c7e34fa78c1b842328ccdbb
-internal/host/activation_offline_linux.go activationOffline.restore authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior; only recorded-active unmasked triggers defer; all other trigger states compare exactly; run7=unfinished done resumes require settled updater exclusion; owned edge mask replacement holds updater lock; replaced units relinquished without file or state mutation; inactive edge never stopped after closure; active owned triggers start under updater lock bb95e89d1bf5363702accd83c21eacefd91f6659b6da62be3f5c39574c907091
+internal/host/activation_offline_linux.go activationOffline.restore authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission before recording; exact recorded state verification; temporary holds checked separately; existing updater exclusion and file ownership retained 1ff1a1548000683e6785ca9b42b871bf0586f0171b6eae603d3b561287562386
internal/host/activation_offline_linux.go activationOffline.save authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 3082a1585eb287687e05756f495aae7de7e11aab6e96bbee920c50b4807ab9b7
internal/host/activation_offline_linux.go activationOffline.status authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 616c3058401766ada7c16395412aaca50b4d68be8d3a2e57ae969a53e21292ff
internal/host/activation_offline_linux.go activationOfflineUnits authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior 372c65c44288c766e9b2aa0ece8cbc55c95fb1ccae7e9bae80a5bfcda2550b4b
@@ -70,9 +70,13 @@ internal/host/activation_adoption_linux.go activationTransactionAllowed authorit
internal/host/activation_adoption_other.go activationTransactionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=all automatic writers excluded through complete; only durable done releases ordinary recovery and repair; absent transaction retains legacy behavior 8c451f30ca31ccca7618f0fd6126ed7d31267c74213c0adb6475e6d504c80723
internal/host/activation_settled_linux.go activationOffline.settled authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior file:Openat@663,file:Stat@1011 176b2ba6cd9eb6af75ded2a7e27c4f6f8cd6ff1107f5949b1c40f884e651644a
internal/host/activation_settled_linux.go activationOffline.updateSettled authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior file:Lstat@317 38adccbf4e50966a54a966af12c3afffc8c02dbcdf43825423bb60ee92eb9c80
-internal/host/activation_offline_linux.go activationOffline.closeDifferences authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior; only recorded-active unmasked triggers defer; all other trigger states compare exactly a6f36043481b7219e399f0711e76b8150f82a24e977f3a648d58e8592b58628b
-internal/host/activation_offline_linux.go activationOffline.reconcile authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior c4f48a941e6cd7ea2d1657ecb7356f1c2978fa879574906af3df3700178d474e
+internal/host/activation_offline_linux.go activationOffline.closeDifferences authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission before recording; exact recorded state verification; temporary holds checked separately; existing updater exclusion and file ownership retained ac9fd3519c2d80a41acd20a0a1d8992d0a17c8cd1c57bf2910cba0d03fb2f559
+internal/host/activation_offline_linux.go activationOffline.reconcile authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission before recording; exact recorded state verification; temporary holds checked separately; existing updater exclusion and file ownership retained 65f7b70c7f5f5e9dec0c5b038f675767eff1d2af3b8ccd78fcc71dc0fb4d5e22
internal/host/activation_settled_linux.go activationOffline.updateDir authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=settled update lock and journal precondition; all writers excluded until done; bounded full close verification with held triggers and masked edge; pending trigger and edge continuation after durable done; absent transaction retains legacy behavior d729025e9d11aa6007ec09318e5e4b84cd14ffda17059d305bbbffe982740fb1
internal/host/activation_settled_linux.go activationOffline.updateUnitsSettled authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator;basis=system update and recovery units must be inactive or failed on admission and resume; transient and unknown states refuse before transaction changes f220fdee955f9a1f27d00f2f7678f143aaf15ac0819e3ec5e208b951b8939c1f
internal/host/activation_offline_linux.go activationRecordedState authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator;basis=exact recorded load and active state; boolean fallback only for older records 99ed5f22fad14f6a8dc572ec7166a1a04415f7067dd1cb4e624b2ad5797947f9
internal/host/activation_settled_linux.go activationConfiguredUpdateRoot authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator;basis=protected host.env binds admission to actual updater lock and journals; conflicting shell override refused 8f9e59c225857ef0601d1f6800bc7e0938f0800589dfaf614c6df93b854dcee1
+internal/host/activation_offline_linux.go activationSupportedBaseline authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission and distinct pre-close hold checks; no automatic claims-off call 038727f4346d66fbda3476733324109ec6ff512fe6fbc6c56cf00c220cf07e12
+internal/host/activation_offline_linux.go activationOffline.baseline authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission and distinct pre-close hold checks; no automatic claims-off call cbcfb2acce3afd20caa3ffa5e89734183342066294e3d196384a85498db9124a
+internal/host/activation_offline_linux.go activationOffline.hold authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission and distinct pre-close hold checks; no automatic claims-off call 75d1d23fc72bdf4697cc29a6381a43320e95ba8697212b179d130372149c910d
+internal/host/activation_offline_linux.go activationOffline.heldDifferences authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=run9 supported-baseline admission and distinct pre-close hold checks; no automatic claims-off call a6cb61a00217d89009ec2436cdce3858d4a75a08bccd84420c92ebf8aab2cf3b