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>
6 files changed,
+437
−84
internal/host/activation_baseline_rootrunner_linux_test.go+274 −0internal/host/activation_invariants_rootrunner_linux_test.go+2 −2internal/host/activation_matrix_rootrunner_linux_test.go+1 −1internal/host/activation_offline_linux.go+135 −70internal/host/activation_production_reboot_rootrunner_linux_test.go+14 −4internal/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 |