carlos / platform Public

Claim activation tools, run 11: remove the automated offline window (not for merge)

Controller ruling: offline-begin, offline-seed and offline-restore removed after six non-converging re-checks; the one-time dev window uses the files seed's approved runbook with manual masking and Plan 14's strict verifyOfflineRootMigration. Copy deletions confirmed by Paul.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c3a212188a17f4dd430e365cd681c796645f90f2 parent b177d91
25 files changed, +110 −4063
  • docs/site/cli.md +1 −3
  • internal/host/activation_adoption_linux.go +0 −56
  • internal/host/activation_adoption_other.go +0 −11
  • internal/host/activation_baseline_linux_test.go +0 −57
  • internal/host/activation_baseline_rootrunner_linux_test.go +0 −287
  • internal/host/activation_command.go +0 −3
  • internal/host/activation_command_linux.go +0 −75
  • internal/host/activation_console_rootrunner_linux_test.go +0 −5
  • internal/host/activation_install_rootrunner_linux_test.go +32 −5
  • internal/host/activation_invariants_rootrunner_linux_test.go +0 −284
  • internal/host/activation_matrix_rootrunner_linux_test.go +0 −198
  • internal/host/activation_offline_linux.go +0 −871
  • internal/host/activation_offline_rootrunner_linux_test.go +0 −270
  • internal/host/activation_production_reboot_rootrunner_linux_test.go +0 −174
  • internal/host/activation_production_rootrunner_linux_test.go +0 −503
  • internal/host/activation_restore_order_rootrunner_linux_test.go +0 −245
  • internal/host/activation_seed_cli_rootrunner_linux_test.go +0 −263
  • internal/host/activation_seed_linux.go +0 −455
  • internal/host/activation_seed_rootrunner_linux_test.go +0 −88
  • internal/host/activation_settled_linux.go +0 −132
  • internal/host/agent.go +0 −3
  • internal/host/systemapply.go +0 −12
  • internal/host/systemunits.go +0 −9
  • internal/provision/plan14_root_reads_test.go +25 −2
  • internal/provision/testdata/plan14-root-reads-activation-tools.tsv +52 −52

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	302.793s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.671s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.269s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.703s
ok  	github.com/carlosframework/platform/internal/activator	250.200s
ok  	github.com/carlosframework/platform/internal/agentsock	0.019s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.452s
ok  	github.com/carlosframework/platform/internal/apperr	0.076s
ok  	github.com/carlosframework/platform/internal/applog	0.140s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.452s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.011s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.699s
ok  	github.com/carlosframework/platform/internal/changelog	0.314s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.032s
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.163s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.430s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.012s
ok  	github.com/carlosframework/platform/internal/dbverify	0.007s
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.370s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.711s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.010s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.134s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.034s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.154s
ok  	github.com/carlosframework/platform/internal/host	174.504s
ok  	github.com/carlosframework/platform/internal/hostactors	0.387s
ok  	github.com/carlosframework/platform/internal/hostbox	2.748s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.526s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	4.759s
ok  	github.com/carlosframework/platform/internal/hostfence	0.037s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.460s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.033s
ok  	github.com/carlosframework/platform/internal/hostops	1.574s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.460s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.141s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.013s
ok  	github.com/carlosframework/platform/internal/instancectl	5.230s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.746s
ok  	github.com/carlosframework/platform/internal/instancefs	0.016s
ok  	github.com/carlosframework/platform/internal/instancekey	0.014s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.024s
ok  	github.com/carlosframework/platform/internal/layout	1.547s
ok  	github.com/carlosframework/platform/internal/lease	9.811s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.017s
ok  	github.com/carlosframework/platform/internal/ledger	0.027s
ok  	github.com/carlosframework/platform/internal/mail	0.167s
ok  	github.com/carlosframework/platform/internal/maintenance	0.084s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.508s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.097s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	21.054s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.785s
ok  	github.com/carlosframework/platform/internal/objstore	0.183s
ok  	github.com/carlosframework/platform/internal/ops	0.172s
ok  	github.com/carlosframework/platform/internal/park	0.202s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	29.819s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.415s
ok  	github.com/carlosframework/platform/internal/relay	7.218s
ok  	github.com/carlosframework/platform/internal/release	21.501s
ok  	github.com/carlosframework/platform/internal/rollout	3.671s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.059s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.568s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.504s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.095s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.261s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.369s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.562s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 48562338 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 04:11:56 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF35C99273E10F, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 29f632816..b1ee9cdc7 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -2156,12 +2156,10 @@ The member command remains `carlos ops host resolve`.
`carlos system claims` prepares the dev claim mechanics trial. Run `help` for the commands and required inputs. `run` executes one revision-checked phase from a protected plan on the trusted signing host; `inspect` returns the verified roster and its signed envelope. Box installers run as root. Add `--dry-run` to preview an operation.
-`offline-begin` persistently masks the migration services, their triggers and the edge. `offline-restore` restores the transaction's own masks and previous active states, with the edge last. `install-control` installs only instance-control and reloads systemd. `console-config` uses a separate console environment file and leaves host execution claims off. Neither installer starts its service.
+`install-control` installs only instance-control and reloads systemd. `console-config` uses a separate console environment file and leaves host execution claims off. Neither installer starts its service.
After starting the guarded console, run `console-report` on its box as root. It checks the signed roster, guard, systemd process and running executable, then records that process in the existing private box report. It refuses stale box evidence and leaves the report’s timestamp unchanged. Run it again after a console restart and immediately before floor publication; the next agent report may replace the process entry.
-`offline-seed` runs files seed in the delegated edge unit while the other migration units remain masked. Its persistent override keeps normal serving off across a reboot. It saves the invocation result, stops the one-shot and masks the edge again before removing the override. Resume an interrupted seed with the same command. `offline-restore` refuses an unfinished seed.
-
#### carlos system journal-migrate
Before starting claim-enabled services on a box with legacy journals, stop the edge and agent. Run `carlos system journal-migrate` as root with `CARLOS_HOST_CLAIM=1` and the deployment environment loaded. The command imports journals only when signed root stop evidence verifies them. It leaves the original files in place. After migration, root ignores changed legacy files and logs an alert. Keep the migration marker.
diff --git a/internal/host/activation_adoption_linux.go b/internal/host/activation_adoption_linux.go
deleted file mode 100644
index f3059ef75..000000000
--- a/internal/host/activation_adoption_linux.go
+++ /dev/null
@@ -1,56 +0,0 @@
-//go:build linux
-
-package host
-
-import (
- "errors"
- "os"
- "path/filepath"
-)
-
-var errActivationRecoveryHeld = errors.New("activation recovery held")
-
-// An explicit offline transaction excludes automatic unit repair until closure.
-// A process lock alone would release that exclusion on interruption or reboot.
-func activationAdoptionAllowed(etc string) error {
- return activationTransactionAllowed(etc, false)
-}
-
-func activationTransactionAllowed(etc string, recovery bool) error {
- statePath := filepath.Join(etc, "carlos", "claim-activation", "offline")
- if _, err := os.Lstat(statePath); errors.Is(err, os.ErrNotExist) {
- return nil
- } else if err != nil {
- return err
- }
- state, err := activationDir(statePath, false)
- if err != nil {
- return err
- }
- defer state.Close()
- st, err := state.Stat()
- if err != nil || st.Mode().Perm() != 0700 {
- return errActivation
- }
- tx := activationOffline{units: filepath.Join(etc, "systemd", "system"), state: statePath}
- r, err := tx.read(state)
- if err != nil {
- return err
- }
- if recovery && r.Phase == "complete" {
- // Edge startup requires this service. While closure owns the units, only
- // an empty recovery may succeed; Run handles this sentinel before effects.
- tx.updateRoot, err = activationConfiguredUpdateRoot(filepath.Join(etc, "carlos", "host.env"), "")
- if err != nil {
- return err
- }
- if err = tx.updateSettled(); err != nil {
- return err
- }
- return errActivationRecoveryHeld
- }
- if r.Phase != "done" {
- return errActivation
- }
- return nil
-}
diff --git a/internal/host/activation_adoption_other.go b/internal/host/activation_adoption_other.go
deleted file mode 100644
index 0f37e7f71..000000000
--- a/internal/host/activation_adoption_other.go
+++ /dev/null
@@ -1,11 +0,0 @@
-//go:build !linux
-
-package host
-
-import "errors"
-
-var errActivationRecoveryHeld = errors.New("activation recovery held")
-
-func activationAdoptionAllowed(string) error { return nil }
-
-func activationTransactionAllowed(string, bool) error { return nil }
diff --git a/internal/host/activation_baseline_linux_test.go b/internal/host/activation_baseline_linux_test.go
deleted file mode 100644
index 1269efff9..000000000
--- a/internal/host/activation_baseline_linux_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-//go:build linux
-
-package host
-
-import (
- "strings"
- "testing"
-)
-
-func TestActivationCanonicalBaseline(t *testing.T) {
- for _, name := range activationOfflineUnits() {
- want := "inactive"
- if name == activationEdgeUnit {
- want = "active"
- }
- for _, trigger := range activationTriggers {
- if name == trigger {
- want = "active"
- }
- }
- for _, active := range []string{"inactive", "active", "failed", "activating"} {
- item := activationOfflineItem{Name: name, LoadState: "loaded", ActiveState: active, Active: active == "active"}
- if got := activationSupportedBaseline(item); got != (active == want) {
- t.Errorf("%s loaded/%s admitted=%t; canonical=%s", name, active, got, want)
- }
- }
- item := activationOfflineItem{Name: name, LoadState: "masked", ActiveState: "inactive", Masked: true}
- wantMask := name != activationEdgeUnit && name != "carlos-system-recover.service"
- if got := activationSupportedBaseline(item); got != wantMask {
- t.Errorf("%s operator mask admitted=%t; want=%t", name, got, wantMask)
- }
- }
-}
-
-func TestActivationCanonicalProductionUnits(t *testing.T) {
- names := activationOfflineUnits()
- if len(names) != len(activationCanonicalBaseline) {
- t.Fatal("canonical set differs from migration set")
- }
- for i, row := range activationCanonicalBaseline {
- if names[i] != row.Name {
- t.Fatal("canonical order", names[i], row.Name)
- }
- raw, err := unitsFS.ReadFile("units/" + row.Name)
- if err != nil {
- t.Fatal(err)
- }
- text := string(raw)
- if strings.HasSuffix(row.Name, ".service") && row.Name != activationEdgeUnit {
- if !strings.Contains(text, "Type=oneshot\n") || strings.Contains(text, "RemainAfterExit=yes") || row.Active != "inactive" {
- t.Fatal("oneshot baseline drift", row.Name)
- }
- } else if row.Active != "active" || !strings.Contains(text, "WantedBy=") {
- t.Fatal("serving baseline drift", row.Name)
- }
- }
-}
diff --git a/internal/host/activation_baseline_rootrunner_linux_test.go b/internal/host/activation_baseline_rootrunner_linux_test.go
deleted file mode 100644
index ab8e34161..000000000
--- a/internal/host/activation_baseline_rootrunner_linux_test.go
+++ /dev/null
@@ -1,287 +0,0 @@
-//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 _, masks := range []bool{false, true} {
- t.Run(fmt.Sprint("operator-masks=", masks), func(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if masks {
- for _, name := range []string{"carlos-stage.path", "carlos-stage.service"} {
- servingRunnerSystemctl(t, "stop", 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")
- }
- if err := tx.begin(t.Context(), true); err != nil {
- t.Fatal(err)
- }
- 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 r.Phase != "done" || r.EdgePending || r.TriggersPending {
- t.Fatal("unfinished", r)
- }
- if diff := tx.closeDifferences(t.Context(), r); len(diff) != 0 {
- t.Fatal(diff)
- }
- assertActivationProductionEdge(t)
- })
- }
-}
-
-func TestRootActivationProductionWholeRecordGate(t *testing.T) {
- for _, persistent := range []bool{false, true} {
- t.Run(fmt.Sprint("persistent=", persistent), func(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- checks := 0
- tx.checkpoint = func(point string) error {
- if point == "verify-completion" {
- checks++
- if persistent || checks == 1 {
- servingRunnerSystemctl(t, "stop", activationEdgeUnit)
- }
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- r := offlineRecord(t, tx)
- if persistent {
- if err == nil || !strings.Contains(err.Error(), activationEdgeUnit) || r.Phase != "complete" || checks != 3 {
- t.Fatalf("closed on drift: phase=%s checks=%d err=%v", r.Phase, checks, err)
- }
- } else if err != nil || r.Phase != "done" || checks != 2 {
- t.Fatalf("did not converge: %s %d %v", r.Phase, checks, err)
- }
- // Both flags are clear here. Every continuation must still verify all units.
- tx.checkpoint = nil
- servingRunnerSystemctl(t, "stop", activationEdgeUnit)
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("resume", err)
- }
- if diff := tx.closeDifferences(t.Context(), offlineRecord(t, tx)); len(diff) != 0 {
- t.Fatal(diff)
- }
- })
- }
-}
-
-func TestRootActivationProductionUnsupportedBaselines(t *testing.T) {
- for _, scenario := range []string{"failed-edge", "masked-active-trigger", "activating-unit", "masked-recovery", "inactive-edge", "inactive-timer", "active-oneshot"} {
- t.Run(scenario, func(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- name, load, active := activationEdgeUnit, "loaded", "failed"
- switch scenario {
- case "masked-recovery":
- name, load, active = "carlos-system-recover.service", "masked", "inactive"
- 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 "inactive-edge", "inactive-timer":
- active = "inactive"
- if scenario == "inactive-timer" {
- name = "carlos-adopt.timer"
- }
- servingRunnerSystemctl(t, "stop", name)
- case "active-oneshot":
- name, active = "carlos-config-sync.service", "active"
- activationProductionWrite(t, filepath.Join(tx.units, name+".d/95-active.conf"), []byte("[Service]\nExecStart=\nExecStart=/bin/true\nRemainAfterExit=yes\n"), 0644)
- servingRunnerSystemctl(t, "daemon-reload")
- servingRunnerSystemctl(t, "start", name)
- 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) {
- tx, _ := offlineFixture(t)
- 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(diff)
- }
- if _, err := tx.systemctl(t.Context(), "stop", activationEdgeUnit); err != nil {
- t.Fatal(err)
- }
- diff := strings.Join(tx.closeDifferences(t.Context(), r), "\n")
- if !strings.Contains(diff, activationEdgeUnit+": load=loaded active=inactive; want load=loaded active=active") {
- t.Fatal("edge omitted", diff)
- }
-}
diff --git a/internal/host/activation_command.go b/internal/host/activation_command.go
index 6bed7773f..1c3b6ebe1 100644
--- a/internal/host/activation_command.go
+++ b/internal/host/activation_command.go
@@ -20,9 +20,6 @@ const activationClaimsUsage = `usage: carlos system claims <command> [flags]
console-config --roster FILE --expect-revision N --bucket B --console-id ID --public-key K [--enable-console-runtime]
console-report --bucket B --aws-region R --expect-revision N --console-id ID --public-key K
install-control
- offline-begin
- offline-seed
- offline-restore
For commands that write, add --dry-run to validate and print the planned operation without writing.
Run signing phases on the trusted operator host. Run box installers as root.
diff --git a/internal/host/activation_command_linux.go b/internal/host/activation_command_linux.go
index b0dd5854b..4293bea40 100644
--- a/internal/host/activation_command_linux.go
+++ b/internal/host/activation_command_linux.go
@@ -7,19 +7,13 @@ import (
"context"
"crypto/ed25519"
"encoding/json"
- "errors"
"flag"
"io"
- "os"
- "os/signal"
- "strconv"
- "syscall"
"github.com/carlosframework/platform/internal/claimactivation"
"github.com/carlosframework/platform/internal/hostactors"
"github.com/carlosframework/platform/internal/hostfloor"
"github.com/carlosframework/platform/internal/release"
- "golang.org/x/sys/unix"
)
func runSystemClaims(args []string, out io.Writer) error {
@@ -257,76 +251,7 @@ func runSystemClaims(args []string, out io.Writer) error {
return err
}
return encode(map[string]any{"dry_run": *dry, "source": "/etc/carlos/host.env", "target": "/etc/systemd/system/" + claimControlUnit, "reload": true, "start": false})
- case "offline-begin", "offline-restore", "offline-seed":
- tx := activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: claimSystemctl, drained: activationProcessesDrained}
- var err error
- tx.updateRoot, err = activationConfiguredUpdateRoot("/etc/carlos/host.env", os.Getenv("CARLOS_OPT_DIR"))
- if err != nil {
- return err
- }
- if args[0] == "offline-seed" {
- seedCtx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
- defer cancel()
- ctx = seedCtx
- tx.seedPreflight = activationSeedPreflight
- err = tx.seed(ctx, []byte(activationSeedUnit), *dry)
- } else if args[0] == "offline-begin" {
- err = tx.begin(ctx, *dry)
- } else {
- err = tx.restore(ctx, *dry)
- }
- if err != nil {
- return err
- }
- preview, err := tx.preview(ctx)
- if err != nil {
- return err
- }
- result := map[string]any{"dry_run": *dry, "phase": args[0], "units": preview, "transaction": tx.state}
- if args[0] == "offline-seed" {
- result["override_path"] = "/etc/systemd/system/carlos-edge.service.d/" + activationSeedDropIn
- result["override"] = activationSeedUnit
- result["guard_dependency_path"] = "/etc/systemd/system/carlos-edge.service.d/90-host-floor.conf"
- result["guard_dependency_replacement"] = activationSeedGuardReplacement
- result["guard_original_path"] = tx.state + "/90-host-floor.conf.original"
- result["result_path"] = tx.state + "/seed-result.txt"
- result["actions"] = []string{"journal-guard-original", "replace-guard-dependency", "install-override", "restore-edge-unit", "reload", "verify-seed-command", "start-seed", "save-result", "stop-seed", "mask-edge", "reload", "restore-guard-original", "remove-override", "reload"}
- }
- return encode(result)
default:
return errActivation
}
}
-func activationProcessesDrained(ctx context.Context) error {
- if err := quiesceLegacyRootMutators(ctx); err != nil {
- return err
- }
- uid, _, err := claimQuiesceOwner()
- if err != nil {
- return err
- }
- entries, err := os.ReadDir("/proc")
- if err != nil {
- return err
- }
- for _, entry := range entries {
- if err = ctx.Err(); err != nil {
- return err
- }
- if _, err = strconv.Atoi(entry.Name()); err != nil {
- continue
- }
- var st unix.Stat_t
- err = unix.Stat("/proc/"+entry.Name(), &st)
- if errors.Is(err, unix.ENOENT) {
- continue
- }
- if err != nil {
- return err
- }
- if int(st.Uid) == uid {
- return errActivation
- }
- }
- return nil
-}
diff --git a/internal/host/activation_console_rootrunner_linux_test.go b/internal/host/activation_console_rootrunner_linux_test.go
index c54084b5e..7b0d5e17e 100644
--- a/internal/host/activation_console_rootrunner_linux_test.go
+++ b/internal/host/activation_console_rootrunner_linux_test.go
@@ -151,11 +151,6 @@ func TestRootActivationConsoleRuntime(t *testing.T) {
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatal("guard arm", string(output), err)
}
- installedGuard, err := os.ReadFile("/etc/systemd/system/carlos-edge.service.d/90-host-floor.conf")
- check(err)
- if string(installedGuard) != activationSeedGuardOriginal {
- t.Fatal("seed fixture differs from guard-arm output")
- }
dbPath := d + "/data/console.db"
st, err := console.Open(dbPath)
check(err)
diff --git a/internal/host/activation_install_rootrunner_linux_test.go b/internal/host/activation_install_rootrunner_linux_test.go
index d5286e5a3..c90890990 100644
--- a/internal/host/activation_install_rootrunner_linux_test.go
+++ b/internal/host/activation_install_rootrunner_linux_test.go
@@ -4,6 +4,7 @@ package host
import (
"bytes"
+ "context"
"crypto/ed25519"
"encoding/json"
"os"
@@ -214,11 +215,37 @@ func TestRootActivationConsoleInstaller(t *testing.T) {
}
func TestRootActivationControlSystemd(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
+ activationRootFixture(t)
+ paths := []string{"/etc/carlos", "/etc/systemd/system/" + claimControlUnit, "/etc/systemd/system/.claims-activation.lock"}
+ for _, name := range rootMigrationOfflineUnits {
+ paths = append(paths, "/etc/systemd/system/"+name)
+ }
+ for _, path := range paths {
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("pre-existing fixture path", path, err)
+ }
+ }
+ t.Cleanup(func() {
+ for _, path := range paths {
+ if err := os.RemoveAll(path); err != nil {
+ t.Error(err)
+ }
+ }
+ if raw, err := claimSystemctl(context.Background(), "daemon-reload"); err != nil {
+ t.Error(string(raw), err)
+ }
+ })
+ if err := os.Mkdir("/etc/carlos", 0700); err != nil {
t.Fatal(err)
}
+ for _, name := range rootMigrationOfflineUnits {
+ if err := os.Symlink("/dev/null", "/etc/systemd/system/"+name); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if raw, err := claimSystemctl(t.Context(), "daemon-reload"); err != nil {
+ t.Fatal(string(raw), err)
+ }
if err := os.WriteFile("/etc/carlos/host.env", []byte("CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM_ORIGINS=1\n"), 0600); err != nil {
t.Fatal(err)
}
@@ -226,14 +253,14 @@ func TestRootActivationControlSystemd(t *testing.T) {
if err := runSystemClaims([]string{"install-control"}, &result); err != nil {
t.Fatal(err)
}
- raw, err := offlineRealSystemctl(t.Context(), "cat", claimControlUnit)
+ raw, err := claimSystemctl(t.Context(), "cat", claimControlUnit)
if err != nil || !bytes.Contains(raw, []byte("MemoryMax=256M")) {
t.Fatal(string(raw), err)
}
if err = verifyOfflineRootMigration(t.Context()); err != nil {
t.Fatal("migration masks changed", err)
}
- raw, err = offlineRealSystemctl(t.Context(), "show", claimControlUnit, "--property=ActiveState", "--value")
+ raw, err = claimSystemctl(t.Context(), "show", claimControlUnit, "--property=ActiveState", "--value")
if err != nil || strings.TrimSpace(string(raw)) != "inactive" {
t.Fatal("control started", string(raw), err)
}
diff --git a/internal/host/activation_invariants_rootrunner_linux_test.go b/internal/host/activation_invariants_rootrunner_linux_test.go
deleted file mode 100644
index 744cf9381..000000000
--- a/internal/host/activation_invariants_rootrunner_linux_test.go
+++ /dev/null
@@ -1,284 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "context"
- "errors"
- "os"
- "path/filepath"
- "strings"
- "testing"
-)
-
-func TestRootActivationSettledPrecondition(t *testing.T) {
- for _, name := range []string{"journal.json", "staging", "rearm-pending.json", "../guard/update-begun.json"} {
- t.Run(name, func(t *testing.T) {
- tx, d := offlineFixture(t)
- tx.updateRoot = filepath.Join(d, "system/router")
- path := filepath.Join(tx.updateRoot, name)
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(path, []byte("pending"), 0600); err != nil {
- t.Fatal(err)
- }
- for _, dry := range []bool{true, false} {
- if err := tx.begin(t.Context(), dry); err == nil || !strings.Contains(err.Error(), "settle the update first") {
- t.Fatal("admitted pending update", dry, err)
- }
- }
- if _, err := os.Stat(tx.state); !os.IsNotExist(err) {
- t.Fatal("refusal created transaction", err)
- }
- if err := os.Remove(path); err != nil {
- t.Fatal(err)
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(path, []byte("pending"), 0600); err != nil {
- t.Fatal(err)
- }
- for _, operation := range []func(context.Context, bool) error{tx.begin, tx.restore} {
- if err := operation(t.Context(), false); err == nil || !strings.Contains(err.Error(), "settle the update first") {
- t.Fatal("resume admitted pending update", err)
- }
- }
- })
- }
-}
-
-func TestRootActivationCloseInvariant(t *testing.T) {
- for _, scenario := range []string{"one-drift", "persistent-drift", "foreign-inode"} {
- t.Run(scenario, func(t *testing.T) {
- tx, _ := offlineFixture(t)
- base := tx.systemctl
- failed := false
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "reset-failed" && args[1] == "carlos-adopt.service" {
- failed = false
- }
- raw, err := base(ctx, args...)
- if args[0] == "show" && args[1] == "carlos-adopt.service" && failed {
- raw = []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState=failed"))
- }
- return raw, err
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- passes := 0
- tx.checkpoint = func(point string) error {
- if point == "verify-close" {
- passes++
- if scenario == "persistent-drift" || passes == 1 {
- failed = true
- }
- if scenario == "foreign-inode" && passes == 1 {
- p := filepath.Join(tx.units, "carlos-adopt.service")
- raw, err := os.ReadFile(p)
- if err != nil {
- return err
- }
- if err := os.WriteFile(p+".foreign", raw, 0644); err != nil {
- return err
- }
- return os.Rename(p+".foreign", p)
- }
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- if scenario == "one-drift" {
- if err != nil || passes != 2 || offlineRecord(t, tx).Phase != "done" {
- t.Fatal("did not reconcile and verify again", passes, err)
- }
- } else {
- if err == nil || !strings.Contains(err.Error(), "carlos-adopt.service") || offlineRecord(t, tx).Phase != "complete" {
- t.Fatal("closed on mismatch", passes, err)
- }
- if target, _ := os.Readlink(filepath.Join(tx.units, activationEdgeUnit)); target != "/dev/null" {
- t.Fatal("released edge")
- }
- }
- })
- }
-}
-
-func TestRootActivationTriggerBeforeFinalGate(t *testing.T) {
- tx, _ := offlineFixture(t)
- base := tx.systemctl
- active := true
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[1%len(args)] == "carlos-adopt.timer" {
- switch args[0] {
- case "start":
- if offlineRecord(t, tx).Phase != "complete" {
- return nil, errors.New("timer started before complete")
- }
- active = true
- case "stop":
- active = false
- }
- }
- raw, err := base(ctx, args...)
- if args[0] == "show" && args[1] == "carlos-adopt.timer" && active {
- raw = []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState=active"))
- }
- return raw, err
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- if !active {
- t.Fatal("active timer not restored")
- }
-}
-
-func TestRootActivationUpdateInProgress(t *testing.T) {
- tx, _ := offlineFixture(t)
- release, err := tx.settled(t.Context(), false)
- if err != nil {
- t.Fatal(err)
- }
- if err := tx.begin(t.Context(), false); err == nil || !strings.Contains(err.Error(), "settle the update first") {
- t.Fatal("update lock admitted", err)
- }
- release()
- base := tx.systemctl
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "show" && args[1] == "carlos-system-update.service" {
- return []byte("LoadState=loaded\nActiveState=activating\n"), nil
- }
- return base(ctx, args...)
- }
- if err := tx.begin(t.Context(), false); err == nil || !strings.Contains(err.Error(), "active=activating") {
- t.Fatal("activating update admitted", err)
- }
-}
-
-func TestRootActivationProductionCloseRefusesInode(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- path := filepath.Join(tx.units, "carlos-stage.path")
- changed := false
- tx.checkpoint = func(point string) error {
- if point == "verify-close" && !changed {
- changed = true
- raw, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- if err := os.WriteFile(path+".foreign", raw, 0644); err != nil {
- return err
- }
- return os.Rename(path+".foreign", path)
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- if err == nil || !strings.Contains(err.Error(), "carlos-stage.path: recorded file inode") {
- t.Fatal("no exact inode refusal", err)
- }
- if r := offlineRecord(t, tx); r.Phase != "complete" {
- t.Fatal("closed on foreign file", r.Phase)
- }
- if load, _, err := tx.status(t.Context(), activationEdgeUnit); err != nil || load != "masked" {
- t.Fatal("edge released", load, err)
- }
- if sameActivationFile(path, filepath.Join(tx.state, "carlos-stage.path.original")) {
- t.Fatal("overwrote foreign inode")
- }
-}
-
-func TestRootActivationRecordedStates(t *testing.T) {
- 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},
- } {
- tx, _ := offlineFixture(t)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- r := offlineRecord(t, tx)
- r.Items[index].LoadState = item.LoadState
- r.Items[index].ActiveState = item.ActiveState
- r.Items[index].Masked = item.Masked
- r.Items[index].Active = item.Active
- if differences := tx.closeDifferences(t.Context(), r); !strings.Contains(strings.Join(differences, "\n"), "want load="+item.LoadState+" active="+item.ActiveState) {
- t.Fatal("lost recorded state", index, differences)
- }
- }
- }
-}
-
-func TestRootActivationConfiguredUpdateRoot(t *testing.T) {
- tx, d := offlineFixture(t)
- config := filepath.Join(d, "host.env")
- custom := filepath.Join(d, "custom-opt")
- if err := os.WriteFile(config, []byte("CARLOS_OPT_DIR="+custom+"\n"), 0600); err != nil {
- t.Fatal(err)
- }
- root, err := activationConfiguredUpdateRoot(config, "")
- if err != nil || root != systemRootDir(custom) {
- t.Fatal("wrong configured update root", root, err)
- }
- if _, err := activationConfiguredUpdateRoot(config, "/opt/carlos"); err == nil {
- t.Fatal("mismatched operator override accepted")
- }
- tx.updateRoot = root
- activationProductionWrite(t, filepath.Join(root, "journal.json"), []byte("pending"), 0600)
- if err := tx.begin(t.Context(), false); err == nil || !strings.Contains(err.Error(), filepath.Join(root, "journal.json")) {
- t.Fatal("configured pending journal missed", err)
- }
- if err := os.WriteFile(config, []byte("CARLOS_OPT_DIR=relative\n"), 0600); err != nil {
- t.Fatal(err)
- }
- if _, err := activationConfiguredUpdateRoot(config, ""); err == nil {
- t.Fatal("relative root accepted")
- }
-}
-
-func TestRootActivationDoneUpdateRace(t *testing.T) {
- tx, _ := offlineFixture(t)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- var held func()
- defer func() {
- if held != nil {
- held()
- }
- }()
- tx.checkpoint = func(point string) error {
- r := offlineRecord(t, tx)
- if point == "edge-clear" && !r.EdgePending && r.TriggersPending {
- var err error
- held, err = tx.settled(t.Context(), false)
- return err
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- if err == nil || !strings.Contains(err.Error(), "settle the update first") {
- t.Fatal("update race admitted", err)
- }
- r := offlineRecord(t, tx)
- if r.Phase != "complete" || r.EdgePending || !r.TriggersPending {
- t.Fatal("lost continuation", r)
- }
- held()
- held = nil
- tx.checkpoint = nil
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("settled retry", err)
- }
-}
diff --git a/internal/host/activation_matrix_rootrunner_linux_test.go b/internal/host/activation_matrix_rootrunner_linux_test.go
deleted file mode 100644
index e083cca9d..000000000
--- a/internal/host/activation_matrix_rootrunner_linux_test.go
+++ /dev/null
@@ -1,198 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "os"
- "path/filepath"
- "strings"
- "testing"
- "time"
-)
-
-const activationMatrixDir = "/var/lib/carlos-activation-matrix"
-
-type activationMatrixCursor struct {
- Cut int
- Resume bool
- Boot, Point string
-}
-
-func activationMatrixTransaction() activationOffline {
- return activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: offlineRealSystemctl, drained: quiesceLegacyRootMutators}
-}
-
-func activationMatrixPending(t *testing.T, tx activationOffline, operation func(context.Context, bool) error) {
- t.Helper()
- path := filepath.Join(tx.updateDir(), "journal.json")
- if _, err := os.Lstat(path); !os.IsNotExist(err) {
- t.Fatal("pre-existing update journal", err)
- }
- activationProductionWrite(t, path, []byte("pending"), 0600)
- before, _ := os.ReadFile(filepath.Join(tx.state, "transaction.json"))
- err := operation(t.Context(), false)
- if err == nil || !strings.Contains(err.Error(), "settle the update first") {
- t.Fatal("pending update admitted", err)
- }
- after, _ := os.ReadFile(filepath.Join(tx.state, "transaction.json"))
- if !bytes.Equal(before, after) {
- t.Fatal("refusal changed transaction")
- }
- if err := os.Remove(path); err != nil {
- t.Fatal(err)
- }
-}
-
-func activationMatrixBaseline(t *testing.T, tx activationOffline) {
- t.Helper()
- servingRunnerSystemctl(t, append([]string{"stop"}, rootMigrationOfflineUnits...)...)
- servingRunnerSystemctl(t, "reset-failed")
- if err := os.RemoveAll(tx.state); err != nil {
- t.Fatal(err)
- }
- servingRunnerSystemctl(t, "start", activationEdgeUnit)
- servingRunnerSystemctl(t, append([]string{"start"}, activationTriggers...)...)
- servingRunnerSystemctl(t, "stop", "carlos-adopt.service")
- servingRunnerSystemctl(t, "reset-failed", "carlos-adopt.service")
- activationMatrixPending(t, tx, tx.begin)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
-}
-
-func activationMatrixCheck(t *testing.T, tx activationOffline, point string) {
- t.Helper()
- r := offlineRecord(t, tx)
- if r.Phase != "complete" && r.Phase != "done" {
- if target, _ := os.Readlink(filepath.Join(tx.units, activationEdgeUnit)); target != "/dev/null" {
- t.Fatal("edge released before complete", point, r.Phase)
- }
- }
- if point == "journal-done" {
- if r.EdgePending || r.TriggersPending {
- t.Fatal("pending work at done")
- }
- if diff := tx.closeDifferences(t.Context(), r); len(diff) != 0 {
- t.Fatal("whole-record mismatch at done", diff)
- }
- }
-}
-
-func activationMatrixInterrupt(t *testing.T, tx activationOffline, cut int) (string, bool) {
- t.Helper()
- n, point := 0, ""
- stop := errors.New("matrix interruption")
- tx.checkpoint = func(name string) error {
- activationMatrixCheck(t, tx, name)
- n++
- if n == cut {
- point = name
- return stop
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- if err != nil && !errors.Is(err, stop) {
- t.Fatal(cut, point, err)
- }
- return point, errors.Is(err, stop)
-}
-
-func activationMatrixResume(t *testing.T, tx activationOffline, point string) {
- t.Helper()
- deadline := time.Now().Add(20 * time.Second)
- for {
- err := tx.restore(t.Context(), false)
- if err == nil {
- break
- }
- if !strings.HasPrefix(err.Error(), "claim activation: settle the update first:") || time.Now().After(deadline) {
- t.Fatal("resume", point, err)
- }
- time.Sleep(50 * time.Millisecond)
- }
- r := offlineRecord(t, tx)
- if r.Phase != "done" || r.EdgePending || r.TriggersPending {
- t.Fatal("unfinished continuation", r)
- }
- if diff := tx.closeDifferences(t.Context(), r); len(diff) != 0 {
- t.Fatal("whole-record completion", point, diff)
- }
- assertActivationProductionEdge(t)
-}
-
-func TestRootActivationProductionInvariantMatrix(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- for cut := 1; cut <= 120; cut++ {
- activationMatrixBaseline(t, tx)
- point, interrupted := activationMatrixInterrupt(t, tx, cut)
- activationMatrixResume(t, tx, point)
- t.Logf("canonical matrix reboot=false cut=%d point=%s", cut, point)
- if !interrupted {
- return
- }
- }
- t.Fatal("matrix did not enumerate all boundaries")
-}
-
-// The runner's persistent unit executes one cut per boot, using real boot IDs.
-func TestRootActivationProductionInvariantBootStep(t *testing.T) {
- var c activationMatrixCursor
- cursor := activationMatrixDir + "/cursor.json"
- raw, err := os.ReadFile(cursor)
- tx := activationMatrixTransaction()
- if os.IsNotExist(err) {
- tx = setupActivationProduction(t)
- c.Cut = 1
- } else if err != nil || json.Unmarshal(raw, &c) != nil {
- t.Fatal("cursor", err)
- }
- boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
- if err != nil {
- t.Fatal(err)
- }
- if c.Resume {
- if c.Boot == string(boot) {
- t.Fatal("no actual reboot")
- }
- if c.Point == "edge-clear" || c.Point == "trigger-complete" {
- if offlineRecord(t, tx).EdgePending {
- t.Fatal("edge flag was not durably clear")
- }
- servingRunnerSystemctl(t, "stop", activationEdgeUnit)
- t.Log("post-boot edge drift with EdgePending=false", c.Point)
- }
- activationMatrixResume(t, tx, c.Point)
- t.Logf("canonical matrix reboot=true cut=%d point=%s boot=%s", c.Cut, c.Point, strings.TrimSpace(string(boot)))
- c.Cut++
- }
- if c.Cut > 120 {
- t.Fatal("unbounded checkpoint matrix")
- }
- activationMatrixBaseline(t, tx)
- point, interrupted := activationMatrixInterrupt(t, tx, c.Cut)
- if !interrupted {
- activationMatrixResume(t, tx, point)
- t.Logf("canonical matrix completion-control cut=%d", c.Cut)
- cleanupActivationProduction(t, tx)
- activationProductionWrite(t, activationMatrixDir+"/control", []byte("done\n"), 0600)
- return
- }
- c.Point, c.Resume, c.Boot = point, true, string(boot)
- raw, err = json.Marshal(c)
- if err != nil {
- t.Fatal(err)
- }
- if err := writeFileDurable(cursor, raw, 0600); err != nil {
- t.Fatal(err)
- }
- if err := writeFileDurable(activationMatrixDir+"/control", []byte("reboot\n"), 0600); err != nil {
- t.Fatal(err)
- }
- t.Logf("actual canonical reboot prepared cut=%d point=%s", c.Cut, point)
-}
diff --git a/internal/host/activation_offline_linux.go b/internal/host/activation_offline_linux.go
deleted file mode 100644
index a6f068bef..000000000
--- a/internal/host/activation_offline_linux.go
+++ /dev/null
@@ -1,871 +0,0 @@
-//go:build linux
-
-package host
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "reflect"
- "strings"
- "syscall"
-
- "github.com/carlosframework/platform/internal/hostfloor"
- "github.com/carlosframework/platform/internal/safefile"
-)
-
-var activationEdgeUnit = "carlos-edge.service"
-
-var activationTriggers = []string{"carlos-adopt.timer", "carlos-adopt.path", "carlos-stage.path", "carlos-restart.path", "carlos-system-update.path"}
-
-func activationOfflineUnits() []string {
- out := append([]string(nil), activationTriggers...)
- out = append(out, rootMigrationOfflineUnits...)
- return append(out, activationEdgeUnit)
-}
-
-type activationOffline struct {
- units, state string
- updateRoot string
- systemctl func(context.Context, ...string) ([]byte, error)
- drained func(context.Context) error
- seedPreflight func(context.Context) error
- checkpoint func(string) error
-}
-type activationOfflineItem struct {
- LoadState string
- ActiveState string
- Name string
- Masked, Active bool
- Inode uint64 `json:",omitempty"`
- Mode uint32
- UID, GID uint32
- Size, Modified int64
- Digest string
-}
-type activationOfflineRecord struct {
- V int
- Units string
- Phase string
- Items []activationOfflineItem
- SeedGuard *activationOfflineItem `json:",omitempty"`
- TriggersPending bool `json:",omitempty"`
- EdgePending bool `json:",omitempty"`
-}
-
-type activationInterruption struct{ error }
-
-func (e activationInterruption) Unwrap() error { return e.error }
-
-func (t activationOffline) point(name string) error {
- if t.checkpoint != nil {
- if err := t.checkpoint(name); err != nil {
- return activationInterruption{err}
- }
- }
- return nil
-}
-func (t activationOffline) dirs(create bool) (*os.File, *os.File, error) {
- if t.state == t.units || strings.HasPrefix(t.state, t.units+"/") {
- return nil, nil, errActivation
- }
- units, err := activationDir(t.units, false)
- if err != nil {
- return nil, nil, err
- }
- state, err := activationDir(t.state, create)
- if err != nil {
- units.Close()
- return nil, nil, err
- }
- st, err := state.Stat()
- if err != nil || st.Mode().Perm() != 0700 {
- units.Close()
- state.Close()
- return nil, nil, errActivation
- }
- return units, state, nil
-}
-func (t activationOffline) status(ctx context.Context, name string) (string, string, error) {
- raw, err := t.systemctl(ctx, "show", name, "--property=LoadState,ActiveState", "--no-pager")
- if err != nil {
- return "", "", err
- }
- fields := map[string]string{}
- for _, line := range strings.Split(string(raw), "\n") {
- k, v, ok := strings.Cut(line, "=")
- if ok {
- if _, seen := fields[k]; seen {
- return "", "", errActivation
- }
- fields[k] = v
- }
- }
- load, active := fields["LoadState"], fields["ActiveState"]
- if load != "loaded" && load != "masked" || active != "active" && active != "inactive" && active != "failed" {
- return load, active, errActivation
- }
- return load, active, nil
-}
-func activationUnitSnapshot(path, name string) (activationOfflineItem, error) {
- item := activationOfflineItem{Name: name}
- st, err := os.Lstat(path)
- if err != nil {
- return item, err
- }
- stat, ok := st.Sys().(*syscall.Stat_t)
- if !ok || stat.Uid != 0 || st.Mode().Perm()&0022 != 0 && st.Mode()&os.ModeSymlink == 0 {
- return item, errActivation
- }
- item.Inode = stat.Ino
- item.Mode = uint32(st.Mode())
- item.UID = stat.Uid
- item.GID = stat.Gid
- item.Size = st.Size()
- item.Modified = st.ModTime().UnixNano()
- if st.Mode()&os.ModeSymlink != 0 {
- target, e := os.Readlink(path)
- if e != nil || target != "/dev/null" {
- return item, errActivation
- }
- item.Masked = true
- item.Digest = hostfloor.Digest([]byte(target))
- return item, nil
- }
- if !st.Mode().IsRegular() || st.Size() > 1<<20 {
- return item, errActivation
- }
- raw, err := os.ReadFile(path)
- if err != nil {
- return item, err
- }
- item.Digest = hostfloor.Digest(raw)
- return item, nil
-}
-func sameActivationFile(a, b string) bool {
- x, e := os.Lstat(a)
- if e != nil {
- return false
- }
- y, e := os.Lstat(b)
- return e == nil && os.SameFile(x, y)
-}
-func (t activationOffline) save(state *os.File, r activationOfflineRecord) error {
- raw, err := json.Marshal(r)
- if err != nil {
- return err
- }
- if err = safefile.WriteAt(int(state.Fd()), "transaction.json", raw, 0600); err != nil {
- return err
- }
- check, err := activationReadAt(state, "transaction.json", true)
- if err != nil || !bytes.Equal(raw, check) {
- return errActivation
- }
- return t.point("journal-" + r.Phase)
-}
-func (t activationOffline) read(state *os.File) (activationOfflineRecord, error) {
- var r activationOfflineRecord
- raw, err := activationReadAt(state, "transaction.json", true)
- if err != nil {
- return r, err
- }
- dec := json.NewDecoder(bytes.NewReader(raw))
- dec.DisallowUnknownFields()
- if err = dec.Decode(&r); err != nil {
- return r, err
- }
- names := activationOfflineUnits()
- if r.V != 1 || r.Units != t.units || len(r.Items) != len(names) {
- return r, errActivation
- }
- for i, item := range r.Items {
- if item.Name != names[i] {
- return r, errActivation
- }
- }
- switch r.Phase {
- case "prepared", "held", "seed-prepared", "restoring", "released", "complete", "done":
- default:
- return r, errActivation
- }
- return r, nil
-}
-
-// units/*.service are non-remaining oneshots; the timer and paths are armed
-// on a healthy box. guardarm.go makes recovery a required edge dependency.
-// Existing inactive operator masks are the only exceptions in this table.
-var activationCanonicalBaseline = []struct {
- Name, Load, Active string
- OperatorMask bool
-}{
- {"carlos-adopt.timer", "loaded", "active", true},
- {"carlos-adopt.path", "loaded", "active", true},
- {"carlos-stage.path", "loaded", "active", true},
- {"carlos-restart.path", "loaded", "active", true},
- {"carlos-system-update.path", "loaded", "active", true},
- {"carlos-adopt.service", "loaded", "inactive", true},
- {"carlos-stage.service", "loaded", "inactive", true},
- {"carlos-config-sync.service", "loaded", "inactive", true},
- {"carlos-restart.service", "loaded", "inactive", true},
- {"carlos-system-update.service", "loaded", "inactive", true},
- {"carlos-system-recover.service", "loaded", "inactive", false},
- {"carlos-edge.service", "loaded", "active", false},
-}
-
-func activationSupportedBaseline(item activationOfflineItem) bool {
- load, active := activationRecordedState(item)
- for _, canonical := range activationCanonicalBaseline {
- if canonical.Name != item.Name {
- continue
- }
- if item.Masked {
- return canonical.OperatorMask && load == "masked" && active == "inactive" && !item.Active
- }
- return load == canonical.Load && active == canonical.Active && item.Active == (active == "active")
- }
- return false
-}
-
-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 _, canonical := range activationCanonicalBaseline {
- name := canonical.Name
- 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; want load=%s active=%s file_mask=false", name, load, active, item.Masked, canonical.Load, canonical.Active)
- if canonical.OperatorMask {
- detail += "; operator_mask=masked/inactive"
- }
- 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"}
- 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")
- if err = os.Link(path, backup); err != nil && !errors.Is(err, os.ErrExist) {
- return r, err
- }
- if !sameActivationFile(path, backup) {
- return r, errActivation
- }
- if !item.Masked {
- mask := filepath.Join(t.state, name+".mask")
- if err = os.Symlink("/dev/null", mask); err != nil && !errors.Is(err, os.ErrExist) {
- return r, err
- }
- st, e := os.Lstat(mask)
- target, e2 := os.Readlink(mask)
- if e != nil || e2 != nil || target != "/dev/null" || st.Sys().(*syscall.Stat_t).Uid != 0 {
- return r, errActivation
- }
- }
- if err = state.Sync(); err != nil {
- return r, err
- }
- r.Items = append(r.Items, item)
- if err = t.point("snapshot-" + name); err != nil {
- return r, err
- }
- }
- return r, t.save(state, r)
-}
-func (t activationOffline) checkItem(item activationOfflineItem) error {
- original := filepath.Join(t.state, item.Name+".original")
- got, err := activationUnitSnapshot(original, item.Name)
- if err != nil {
- return err
- }
- if item.Inode == 0 {
- got.Inode = 0
- }
- got.Active = item.Active
- got.LoadState = item.LoadState
- got.ActiveState = item.ActiveState
- if !reflect.DeepEqual(got, item) {
- return errActivation
- }
- current := filepath.Join(t.units, item.Name)
- if sameActivationFile(current, original) {
- return nil
- }
- if !item.Masked && sameActivationFile(current, filepath.Join(t.state, item.Name+".mask")) {
- return nil
- }
- return errActivation
-}
-func (t activationOffline) replace(units, state *os.File, item activationOfflineItem, restore bool) error {
- if err := t.checkItem(item); err != nil {
- return err
- }
- if item.Masked {
- return nil
- }
- source := filepath.Join(t.state, item.Name+".mask")
- if restore {
- source = filepath.Join(t.state, item.Name+".original")
- }
- target := filepath.Join(t.units, item.Name)
- if sameActivationFile(source, target) {
- return nil
- }
- stage := filepath.Join(t.state, item.Name+".next")
- if err := os.Link(source, stage); err != nil && !errors.Is(err, os.ErrExist) {
- return err
- }
- if !sameActivationFile(source, stage) {
- return errActivation
- }
- if err := state.Sync(); err != nil {
- return err
- }
- if err := t.point("stage-" + item.Name); err != nil {
- return err
- }
- if err := os.Rename(stage, target); err != nil {
- return err
- }
- if err := units.Sync(); err != nil {
- return err
- }
- if err := state.Sync(); err != nil {
- return err
- }
- if err := t.point("replace-" + item.Name); err != nil {
- return err
- }
- if !sameActivationFile(source, target) {
- return errActivation
- }
- 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
- }
- defer release()
- if dry {
- state, err := activationDir(t.state, false)
- if err == nil {
- record, e := t.read(state)
- state.Close()
- if e == nil {
- if record.Phase != "prepared" && record.Phase != "held" {
- return errActivation
- }
- for _, item := range record.Items {
- if e = t.checkItem(item); e != nil {
- return e
- }
- }
- } else if !errors.Is(e, os.ErrNotExist) {
- return e
- }
- } else if !errors.Is(err, os.ErrNotExist) {
- return err
- }
-
- units, err := activationDir(t.units, false)
- if err != nil {
- return err
- }
- defer units.Close()
- for _, name := range activationOfflineUnits() {
- if _, err := activationUnitSnapshot(filepath.Join(t.units, name), name); err != nil {
- return err
- }
- if _, _, err := t.status(ctx, name); err != nil {
- return err
- }
- }
- return nil
- }
- units, state, err := t.dirs(true)
- if err != nil {
- return err
- }
- defer units.Close()
- defer state.Close()
- lock, err := activationLock(state)
- if err != nil {
- return err
- }
- defer lock.Close()
- r, err := t.read(state)
- if errors.Is(err, os.ErrNotExist) {
- r, err = t.prepare(ctx, units, state)
- }
- if err != nil {
- return err
- }
- if r.Phase != "prepared" && r.Phase != "held" {
- return errActivation
- }
- for _, item := range r.Items {
- if err = t.checkItem(item); err != nil {
- return err
- }
- }
- for _, item := range r.Items {
- if _, err = t.systemctl(ctx, "stop", item.Name); err != nil {
- return err
- }
- if err = t.point("stop-" + item.Name); err != nil {
- return err
- }
- }
- for _, item := range r.Items {
- if err = t.replace(units, state, item, false); err != nil {
- return err
- }
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("reload"); err != nil {
- return err
- }
- for _, item := range r.Items {
- load, active, e := t.status(ctx, item.Name)
- if e != nil {
- return e
- }
- if load != "masked" || active != "inactive" && active != "failed" {
- return errActivation
- }
- }
- if t.drained == nil {
- return errActivation
- }
- if err = t.drained(ctx); err != nil {
- return err
- }
- if err = t.point("drained"); err != nil {
- return err
- }
- r.Phase = "held"
- return t.save(state, r)
-}
-func (t activationOffline) restore(ctx context.Context, dry bool) error {
- units, state, err := t.dirs(false)
- if err != nil {
- return err
- }
- defer units.Close()
- defer state.Close()
- if !dry {
- lock, e := activationLock(state)
- if e != nil {
- return e
- }
- defer lock.Close()
- }
- r, err := t.read(state)
- if err != nil {
- return err
- }
- if r.Phase == "prepared" || r.Phase == "seed-prepared" {
- return errActivation
- }
- release, err := t.settled(ctx, dry)
- if err != nil {
- return err
- }
- defer func() { release() }()
- if r.Phase == "done" {
- r.Phase = "complete"
- if !dry {
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- }
- if r.Phase != "done" {
- for _, item := range r.Items {
- if err = t.checkItem(item); err != nil {
- return fmt.Errorf("%s: recorded file inode, content, or metadata changed: %w", item.Name, err)
- }
- }
- }
- if dry {
- return nil
- }
-
- edge := r.Items[len(r.Items)-1]
- if r.Phase != "done" {
- // A resumed complete record still owns every unit. Reconcile them all:
- // boot may have started an enabled service or left recovery failed.
- if err = t.replace(units, state, edge, false); err != nil {
- return err
- }
- if r.Phase != "complete" {
- r.Phase = "restoring"
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- for _, item := range r.Items[:len(activationTriggers)] {
- if _, err = t.systemctl(ctx, "stop", item.Name); err != nil {
- return err
- }
- if err = t.point("hold-" + item.Name); err != nil {
- return err
- }
- }
- for _, item := range r.Items[len(activationTriggers) : len(r.Items)-1] {
- if err = t.replace(units, state, item, true); err != nil {
- return err
- }
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("restore-reload"); err != nil {
- return err
- }
- if r.Phase != "complete" {
- r.Phase = "released"
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- for _, item := range r.Items[:len(activationTriggers)] {
- if err = t.replace(units, state, item, true); err != nil {
- return err
- }
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("trigger-reload"); err != nil {
- return err
- }
- if r.Phase != "complete" {
- // All unit writers remain excluded until verified closure.
- r.Phase = "complete"
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- // Verify the temporary holds before releasing the edge and triggers.
- // complete retains writer exclusion until the whole-record gate passes.
- var differences []string
- const rounds = 3
- for round := 0; round < rounds; round++ {
- differences = nil
- for i, item := range r.Items {
- if item.Name == activationEdgeUnit || i < len(activationTriggers) && item.Active {
- err = t.hold(ctx, item.Name)
- } else {
- err = t.reconcile(ctx, item)
- }
- if err != nil {
- var interrupted activationInterruption
- if errors.As(err, &interrupted) {
- return err
- }
- differences = append(differences, err.Error())
- }
- }
- if err = t.point("verify-close"); err != nil {
- return err
- }
- differences = append(differences, t.heldDifferences(ctx, r)...)
- if len(differences) == 0 {
- break
- }
- }
- if len(differences) != 0 {
- return fmt.Errorf("claim activation: restore remains complete after %d verification rounds:\n%s", rounds, strings.Join(differences, "\n"))
- }
- r.Phase, r.EdgePending, r.TriggersPending = "complete", true, true
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- // complete still owns every original. Pending flags record progress only;
- // they never suppress the final comparison on resume.
- if err = t.replace(units, state, edge, true); err != nil {
- return err
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("edge-reload"); err != nil {
- return err
- }
- release()
- release = func() {}
- if err = t.reconcile(ctx, edge); err != nil {
- return err
- }
- r.EdgePending = false
- if err = t.save(state, r); err != nil {
- return err
- }
- if err = t.point("edge-clear"); err != nil {
- return err
- }
- nextRelease, e := t.settled(ctx, false)
- if e != nil {
- return e
- }
- release = nextRelease
- for _, item := range r.Items[:len(activationTriggers)] {
- if err = t.reconcile(ctx, item); err != nil {
- return err
- }
- }
- r.TriggersPending = false
- if err = t.save(state, r); err != nil {
- return err
- }
- if err = t.point("trigger-complete"); err != nil {
- return err
- }
- const rounds = 3
- var differences []string
- for round := 0; round < rounds; round++ {
- if round > 0 {
- // Reconcile only retained originals, and leave the edge until last.
- for _, item := range r.Items {
- if t.checkItem(item) != nil {
- continue
- }
- if err = t.reconcile(ctx, item); err != nil {
- var interrupted activationInterruption
- if errors.As(err, &interrupted) {
- return err
- }
- }
- }
- }
- if err = t.point("verify-completion"); err != nil {
- return err
- }
- differences = t.closeDifferences(ctx, r)
- if len(differences) == 0 {
- r.Phase = "done"
- return t.save(state, r)
- }
- }
- return fmt.Errorf("claim activation: restore remains complete after %d verification rounds:\n%s", rounds, strings.Join(differences, "\n"))
-}
-
-type activationOfflinePreview struct {
- Unit string `json:"unit"`
- OriginalSHA256 string `json:"original_sha256"`
- PreserveMask bool `json:"preserve_mask"`
- RestoreActive bool `json:"restore_active"`
- CurrentLoadState string `json:"current_load_state"`
- CurrentActiveState string `json:"current_active_state"`
-}
-
-func (t activationOffline) preview(ctx context.Context) ([]activationOfflinePreview, error) {
- var items []activationOfflineItem
- state, err := activationDir(t.state, false)
- if err == nil {
- defer state.Close()
- record, e := t.read(state)
- if e == nil {
- items = record.Items
- } else if !errors.Is(e, os.ErrNotExist) {
- return nil, e
- }
- } else if !errors.Is(err, os.ErrNotExist) {
- return nil, err
- }
- if items == nil {
- units, e := activationDir(t.units, false)
- if e != nil {
- return nil, e
- }
- defer units.Close()
- for _, name := range activationOfflineUnits() {
- item, e := activationUnitSnapshot(filepath.Join(t.units, name), name)
- if e != nil {
- return nil, e
- }
- load, active, e := t.status(ctx, name)
- if e != nil {
- return nil, e
- }
- item.LoadState = load
- item.ActiveState = active
- item.Active = active == "active"
- items = append(items, item)
- }
- }
- var out []activationOfflinePreview
- for _, item := range items {
- load, active, err := t.status(ctx, item.Name)
- if err != nil {
- return nil, err
- }
- out = append(out, activationOfflinePreview{Unit: item.Name, OriginalSHA256: item.Digest, PreserveMask: item.Masked, RestoreActive: item.Active, CurrentLoadState: load, CurrentActiveState: active})
- }
- return out, nil
-}
-
-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("stop-" + name); err != nil {
- return err
- }
- _, active, err := t.status(ctx, name)
- if err != nil {
- 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)
- }
- _, 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("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
-}
-
-// 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 {
- 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, 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))
- }
- 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))
- }
- }
- if r.SeedGuard != nil {
- g := activationOffline{units: filepath.Join(t.units, "carlos-edge.service.d"), state: t.state}
- if err := g.checkItem(*r.SeedGuard); err != nil || !sameActivationFile(filepath.Join(g.units, r.SeedGuard.Name), filepath.Join(t.state, r.SeedGuard.Name+".original")) {
- differences = append(differences, fmt.Sprintf("%s: recorded file inode, content, or metadata changed", r.SeedGuard.Name))
- }
- }
- if err := t.updateSettled(); err != nil {
- differences = append(differences, err.Error())
- }
- return differences
-}
-
-func activationRecordedState(item activationOfflineItem) (string, string) {
- load, active := item.LoadState, item.ActiveState
- if load == "" {
- load = "loaded"
- if item.Masked {
- load = "masked"
- }
- }
- if active == "" {
- active = "inactive"
- if item.Active {
- active = "active"
- }
- }
- return load, active
-}
diff --git a/internal/host/activation_offline_rootrunner_linux_test.go b/internal/host/activation_offline_rootrunner_linux_test.go
deleted file mode 100644
index 1a74a9c70..000000000
--- a/internal/host/activation_offline_rootrunner_linux_test.go
+++ /dev/null
@@ -1,270 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "syscall"
- "testing"
-)
-
-func offlineFixture(t *testing.T) (activationOffline, string) {
- t.Helper()
- d := activationRootFixture(t)
- units := filepath.Join(d, "units")
- os.Mkdir(units, 0755)
- state := filepath.Join(d, "transaction")
- for _, name := range activationOfflineUnits() {
- os.WriteFile(filepath.Join(units, name), []byte("[Unit]\nDescription="+name+"\n"), 0644)
- }
- os.Remove(filepath.Join(units, "carlos-stage.service"))
- os.Symlink("/dev/null", filepath.Join(units, "carlos-stage.service"))
- active := map[string]bool{activationEdgeUnit: true}
- for _, name := range activationTriggers {
- active[name] = true
- }
- ctl := func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "start" {
- active[args[1]] = true
- }
- if args[0] == "stop" {
- active[args[1]] = false
- }
- if args[0] == "show" {
- load := "loaded"
- if target, _ := os.Readlink(filepath.Join(units, args[1])); target == "/dev/null" {
- load = "masked"
- }
- state := "inactive"
- if active[args[1]] {
- state = "active"
- }
- return []byte("LoadState=" + load + "\nActiveState=" + state + "\n"), nil
- }
- return nil, nil
- }
- return activationOffline{units: units, state: state, updateRoot: filepath.Join(d, "system/router"), systemctl: ctl, drained: func(context.Context) error { return nil }}, d
-}
-func TestRootActivationModelOfflineInterruptions(t *testing.T) {
- for _, phase := range []string{"begin", "restore"} {
- for cut := 1; cut < 100; cut++ {
- t.Run(fmt.Sprintf("%s/%d", phase, cut), func(t *testing.T) {
- tx, _ := offlineFixture(t)
- active := map[string]bool{"carlos-adopt.timer": true, "carlos-edge.service": true}
- base := tx.systemctl
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "stop" {
- active[args[1]] = false
- }
- if args[0] == "start" {
- active[args[1]] = true
- }
- raw, err := base(ctx, args...)
- if args[0] == "show" && active[args[1]] {
- raw = []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState=active"))
- }
- return raw, err
- }
- if phase == "restore" {
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- }
- n := 0
- injected := errors.New("interrupted")
- tx.checkpoint = func(string) error {
- n++
- if n == cut {
- return injected
- }
- return nil
- }
- var err error
- if phase == "begin" {
- err = tx.begin(t.Context(), false)
- } else {
- err = tx.restore(t.Context(), false)
- }
- if err != nil && !errors.Is(err, injected) {
- t.Fatal(err)
- }
- tx.checkpoint = nil
- if phase == "begin" {
- if err = tx.begin(t.Context(), false); err != nil {
- t.Fatal("resume", err)
- }
- }
- if err = tx.restore(t.Context(), false); err != nil {
- t.Fatal("restore", err)
- }
- if err = tx.begin(t.Context(), true); err == nil {
- t.Fatal("dry run reopened completed transaction")
- }
-
- for _, name := range activationOfflineUnits() {
- path := filepath.Join(tx.units, name)
- if name == "carlos-stage.service" {
- target, e := os.Readlink(path)
- if e != nil || target != "/dev/null" {
- t.Fatal("operator mask lost")
- }
- continue
- }
- raw, e := os.ReadFile(path)
- if e != nil || string(raw) != "[Unit]\nDescription="+name+"\n" {
- t.Fatal("original lost", name, e)
- }
- }
- if n < cut {
- cut = 100
- }
- })
- }
- }
-}
-func TestRootActivationModelReplacementRefused(t *testing.T) {
- tx, _ := offlineFixture(t)
- if err := tx.begin(t.Context(), true); err != nil {
- t.Fatal(err)
- }
- if _, err := os.Stat(tx.state); !os.IsNotExist(err) {
- t.Fatal("dry run wrote state")
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- p := filepath.Join(tx.units, "carlos-edge.service")
- os.Remove(p)
- os.Symlink("/dev/null", p)
- if err := tx.restore(t.Context(), false); err == nil {
- t.Fatal("unowned replacement removed")
- }
- if target, _ := os.Readlink(p); target != "/dev/null" {
- t.Fatal("replacement mask changed")
- }
-}
-
-const offlineRebootState = "/etc/carlos-activation-rootrunner"
-
-func offlineRealSystemctl(ctx context.Context, args ...string) ([]byte, error) {
- return exec.CommandContext(ctx, "systemctl", args...).CombinedOutput()
-}
-func TestRootActivationModelEdgeLast(t *testing.T) {
- tx, _ := offlineFixture(t)
- original := tx.systemctl
- restoring := false
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "show" && (args[1] == "carlos-edge.service" || args[1] == "carlos-adopt.timer") {
- if !restoring {
- return []byte("LoadState=loaded\nActiveState=active\n"), nil
- }
- }
- return original(ctx, args...)
- }
- units, state, err := tx.dirs(true)
- if err != nil {
- t.Fatal(err)
- }
- _, err = tx.prepare(t.Context(), units, state)
- units.Close()
- state.Close()
- if err != nil {
- t.Fatal(err)
- }
- tx.systemctl = original
- if err = tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- restoring = true
- started := map[string]bool{}
- reconciled := map[string]bool{}
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- if args[0] == "stop" {
- reconciled[args[1]] = true
- }
- if args[0] == "start" {
- for _, trigger := range activationTriggers {
- if args[1] == trigger {
- for _, service := range rootMigrationOfflineUnits {
- if !reconciled[service] {
- t.Fatalf("started %s before reconciling %s", trigger, service)
- }
- }
- }
- }
- if args[1] == "carlos-edge.service" && offlineRecord(t, tx).Phase != "complete" {
- t.Fatal("edge started before durable completion")
- }
-
- started[args[1]] = true
- }
- if args[0] == "show" && started[args[1]] {
- return []byte("LoadState=loaded\nActiveState=active\n"), nil
- }
- return original(ctx, args...)
- }
- if err = tx.restore(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- if !started["carlos-edge.service"] || !started["carlos-adopt.timer"] {
- t.Fatal("active states not restored", started)
- }
-}
-
-func TestRootActivationProcessDrain(t *testing.T) {
- uid, gid := generationRunnerRegistryUser(t)
- cmd := exec.Command("/usr/bin/sleep", "60")
- cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
- if err := cmd.Start(); err != nil {
- t.Fatal(err)
- }
- defer func() { cmd.Process.Kill(); cmd.Wait() }()
- if err := activationProcessesDrained(t.Context()); err == nil {
- t.Fatal("live service process accepted")
- }
- if err := cmd.Process.Kill(); err != nil {
- t.Fatal(err)
- }
- cmd.Wait()
- if err := activationProcessesDrained(t.Context()); err != nil {
- t.Fatal("drained service refused", err)
- }
-}
-
-func TestRootActivationModelReleasedTriggerOwnership(t *testing.T) {
- tx, _ := offlineFixture(t)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- injected := errors.New("released")
- tx.checkpoint = func(name string) error {
- if name == "journal-released" {
- return injected
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); !errors.Is(err, injected) {
- t.Fatal(err)
- }
- tx.checkpoint = nil
- p := filepath.Join(tx.units, "carlos-adopt.timer")
- if err := os.WriteFile(p+".operator", []byte("operator replacement"), 0644); err != nil {
- t.Fatal(err)
- }
- if err := os.Rename(p+".operator", p); err != nil {
- t.Fatal(err)
- }
- if err := tx.restore(t.Context(), false); err == nil {
- t.Fatal("claimed authority over replaced trigger")
- }
- got, _ := os.ReadFile(p)
- if string(got) != "operator replacement" {
- t.Fatal("replacement changed")
- }
-}
diff --git a/internal/host/activation_production_reboot_rootrunner_linux_test.go b/internal/host/activation_production_reboot_rootrunner_linux_test.go
deleted file mode 100644
index 81daeaab4..000000000
--- a/internal/host/activation_production_reboot_rootrunner_linux_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "bytes"
- "errors"
- "os"
- "path/filepath"
- "strings"
- "testing"
-)
-
-func activationProductionRebootPhase1(t *testing.T, phase string) {
- tx := setupActivationProduction(t)
-
- stop := errors.New("reboot checkpoint")
- point := "journal-" + phase
- tx.checkpoint = func(name string) error {
- if name == point {
- return stop
- }
- return nil
- }
- err := tx.begin(t.Context(), false)
- if phase != "prepared" && phase != "held" {
- if err != nil {
- t.Fatal(err)
- }
- err = tx.restore(t.Context(), false)
- }
- if !errors.Is(err, stop) {
- t.Fatal("checkpoint not reached", point, err)
- }
- r := offlineRecord(t, tx)
- if r.Phase != phase {
- t.Fatal("phase", r.Phase, phase)
- }
- if phase != "prepared" && phase != "done" {
- load, active, err := tx.status(t.Context(), activationEdgeUnit)
- if err != nil || load != "masked" || active != "inactive" {
- t.Fatal("edge escaped before release", load, active, err)
- }
- }
- if phase == "done" {
- if r.EdgePending || r.TriggersPending {
- t.Fatal("pending work at done")
- }
- if diff := tx.closeDifferences(t.Context(), r); len(diff) != 0 {
- t.Fatal(diff)
- }
- }
-
- boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
- if err != nil {
- t.Fatal(err)
- }
- activationProductionWrite(t, offlineRebootState+"/boot", boot, 0600)
- activationProductionWrite(t, offlineRebootState+"/phase", []byte(phase), 0600)
- t.Log("rebooting production units at", point)
-}
-
-func activationProductionRebootPhase2(t *testing.T, phase string) {
- tx := activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: offlineRealSystemctl, drained: quiesceLegacyRootMutators}
- defer cleanupActivationProduction(t, tx)
- before, err := os.ReadFile(offlineRebootState + "/boot")
- if err != nil {
- t.Fatal(err)
- }
- after, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
- if err != nil || bytes.Equal(before, after) {
- t.Fatal("no actual reboot", err)
- }
- r := offlineRecord(t, tx)
- if r.Phase != phase {
- t.Fatal("durable phase changed", r.Phase, phase)
- }
- if phase != "prepared" && phase != "done" {
- load, active, err := tx.status(t.Context(), activationEdgeUnit)
- if err != nil || load != "masked" || active != "inactive" {
- t.Fatal("edge escaped on reboot", load, active, err)
- }
- }
- if phase == "prepared" {
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal("resume begin", err)
- }
- }
- 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 {
- t.Fatal(err)
- }
- for _, name := range rootMigrationOfflineUnits {
- tx.systemctl(t.Context(), "start", name)
- }
- if err := os.Rename("/etc/carlos/host.env.saved", "/etc/carlos/host.env"); err != nil {
- t.Fatal(err)
- }
- servingRunnerSystemctl(t, "start", "carlos-adopt.timer")
- }
-
- tx.checkpoint = func(point string) error {
- if point == "replace-"+activationEdgeUnit || point == "start-"+activationEdgeUnit {
- if offlineRecord(t, tx).Phase != "complete" {
- t.Fatal("edge released before done", point)
- }
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("resume after actual reboot", phase, err)
- }
- assertActivationProductionRecorded(t, tx, true)
- assertActivationProductionEdge(t)
- if got := offlineRecord(t, tx); got.Phase != "done" || got.EdgePending {
- t.Fatal("unfinished", got.Phase, got.EdgePending)
- }
- for _, name := range []string{"carlos-edge.service", "carlos-system-recover.service"} {
- raw, err := os.ReadFile(filepath.Join(tx.units, name+".d/90-host-floor.conf"))
- if err != nil || !strings.Contains(string(raw), "guard-exec") {
- t.Fatal("lost production guard", name, err)
- }
- }
- t.Log("actual reboot and production restore passed", phase)
- if phase == "restoring" {
- cleanupActivationProduction(t, tx)
- t.Run("seed-production-units", TestRootActivationSeedCLI)
- }
-}
-
-func TestRootRebootActivationProductionPreparedPhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "prepared")
-}
-func TestRootRebootActivationProductionPreparedPhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "prepared")
-}
-func TestRootRebootActivationProductionHeldPhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "held")
-}
-func TestRootRebootActivationProductionHeldPhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "held")
-}
-func TestRootRebootActivationProductionRestoringPhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "restoring")
-}
-func TestRootRebootActivationProductionRestoringPhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "restoring")
-}
-func TestRootRebootActivationProductionReleasedPhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "released")
-}
-func TestRootRebootActivationProductionReleasedPhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "released")
-}
-func TestRootRebootActivationProductionCompletePhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "complete")
-}
-func TestRootRebootActivationProductionCompletePhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "complete")
-}
-func TestRootRebootActivationProductionDonePhase1(t *testing.T) {
- activationProductionRebootPhase1(t, "done")
-}
-func TestRootRebootActivationProductionDonePhase2(t *testing.T) {
- activationProductionRebootPhase2(t, "done")
-}
diff --git a/internal/host/activation_production_rootrunner_linux_test.go b/internal/host/activation_production_rootrunner_linux_test.go
deleted file mode 100644
index dea8fa1ef..000000000
--- a/internal/host/activation_production_rootrunner_linux_test.go
+++ /dev/null
@@ -1,503 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "context"
- "crypto/ed25519"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "os/exec"
- "os/user"
- "path/filepath"
- "strconv"
- "strings"
- "testing"
- "time"
-
- "github.com/carlosframework/platform/internal/hostactors"
- "github.com/carlosframework/platform/internal/hostfloor"
- "github.com/carlosframework/platform/internal/release"
-)
-
-func activationProductionWrite(t *testing.T, path string, raw []byte, mode os.FileMode) {
- t.Helper()
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(path, raw, mode); err != nil {
- t.Fatal(err)
- }
-}
-
-func activationProductionNames() []string {
- return append(activationOfflineUnits(), "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos.slice", "carlos-instance-control.service", "carlos-instance-control.socket")
-}
-
-func installActivationProductionUnits(t *testing.T, uid, gid int) activationOffline {
- t.Helper()
- if err := os.Mkdir(offlineRebootState, 0755); err != nil {
- t.Fatal(err)
- }
- for _, path := range []string{"/data", "/run/carlos", "/run/carlos-control"} {
- if _, err := os.Lstat(path); !os.IsNotExist(err) {
- t.Fatal("pre-existing", path, err)
- }
- }
- for _, name := range activationProductionNames() {
- path := "/etc/systemd/system/" + name
- if _, err := os.Lstat(path); !os.IsNotExist(err) {
- t.Fatal("pre-existing", path, err)
- }
- raw, err := unitsFS.ReadFile("units/" + name)
- if err != nil {
- t.Fatal(err)
- }
- activationProductionWrite(t, path, raw, 0644)
- if strings.HasSuffix(name, ".service") {
- activationProductionWrite(t, path+".d/10-runner-network.conf", []byte("[Service]\nIPAddressDeny=any\nIPAddressAllow=127.0.0.1\n"), 0644)
- }
- }
- for _, path := range []string{"/data", "/data/.instances", "/data/instances", "/data/layout", "/run/carlos", "/run/carlos/.instances", "/run/carlos/instances", "/run/carlos/.logs", "/run/carlos/logs"} {
- if err := os.MkdirAll(path, 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.Chown(path, uid, gid); err != nil {
- t.Fatal(err)
- }
- }
- var tmp strings.Builder
- for _, path := range []string{"/run/carlos", "/run/carlos/.instances", "/run/carlos/instances", "/run/carlos/.logs", "/run/carlos/logs"} {
- fmt.Fprintf(&tmp, "d %s 0755 carlos carlos -\n", path)
- }
- fmt.Fprintln(&tmp, "d /run/carlos-control 0750 root carlos -")
- activationProductionWrite(t, "/etc/tmpfiles.d/carlos-activation-runner.conf", []byte(tmp.String()), 0644)
- return activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: offlineRealSystemctl, drained: quiesceLegacyRootMutators}
-}
-
-func cleanupActivationProduction(t *testing.T, tx activationOffline) {
- t.Helper()
- if t.Failed() {
- out, _ := exec.Command("journalctl", "-u", "carlos-edge.service", "-u", "carlos-system-recover.service", "-u", "data-instances.mount", "-u", "run-carlos-instances.mount", "-u", "run-carlos-logs.mount", "--no-pager", "-n", "80").CombinedOutput()
- t.Log(string(out))
- }
- for _, name := range activationProductionNames() {
- offlineRealSystemctl(context.Background(), "disable", "--now", name)
- os.Remove("/etc/systemd/system/" + name)
- os.RemoveAll("/etc/systemd/system/" + name + ".d")
- }
- offlineRealSystemctl(context.Background(), "daemon-reload")
- offlineRealSystemctl(context.Background(), "reset-failed")
- for _, path := range []string{"/data", "/run/carlos", "/run/carlos-control", "/etc/carlos", "/opt/carlos", "/etc/tmpfiles.d/carlos-activation-runner.conf", offlineRebootState} {
- os.RemoveAll(path)
- }
- if _, err := os.Stat("/etc/carlos-activation-runner-user"); err == nil {
- if out, err := exec.Command("userdel", serviceUser).CombinedOutput(); err != nil {
- t.Errorf("remove fixture user: %s %v", out, err)
- return
- }
- if _, err := user.LookupGroup(serviceUser); err == nil {
- if out, err := exec.Command("groupdel", serviceUser).CombinedOutput(); err != nil {
- t.Errorf("remove fixture group: %s %v", out, err)
- return
- }
- }
- os.Remove("/etc/carlos-activation-runner-user")
- }
-
-}
-
-func assertActivationProductionEdge(t *testing.T) {
- t.Helper()
- ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
- defer cancel()
- for {
- out, err := exec.CommandContext(ctx, "curl", "-s", "--max-time", "1", "-o", "/dev/null", "http://127.0.0.1:18080").CombinedOutput()
- if err == nil {
- break
- }
- if ctx.Err() != nil {
- t.Fatal("real edge did not listen", string(out), err)
- }
- time.Sleep(100 * time.Millisecond)
- }
- raw, err := offlineRealSystemctl(ctx, "show", "carlos-edge.service", "--property=Requires,ExecStart")
- if err != nil || !strings.Contains(string(raw), "carlos-system-recover.service") || !strings.Contains(string(raw), "guard-exec edge") {
- t.Fatal("production edge dependency missing", string(raw), err)
- }
- raw, err = offlineRealSystemctl(ctx, "show", "carlos-system-recover.service", "--property=ExecStart,Result")
- if err != nil || !strings.Contains(string(raw), "guard-exec recover") || !strings.Contains(string(raw), "Result=success") {
- t.Fatal("real guarded recovery did not succeed", string(raw), err)
- }
-}
-
-func setupActivationProduction(t *testing.T) activationOffline {
- t.Helper()
- if len(activationCarlosImage) == 0 {
- t.Fatal("requires embedded real CLI")
- }
- uid, gid := activationProductionUser(t)
- for _, path := range []string{"/etc/carlos", "/opt/carlos", "/etc/systemd/system/carlos-edge.service.d", "/etc/systemd/system/carlos-instance-control.service", "/etc/systemd/system/carlos-system-update.service.d", "/etc/systemd/system/carlos-system-recover.service.d"} {
- if _, err := os.Lstat(path); !os.IsNotExist(err) {
- t.Fatal("existing fixture path", path, err)
- }
- }
- tx := installActivationProductionUnits(t, uid, gid)
- t.Cleanup(func() {
- if t.Failed() {
- cleanupActivationProduction(t, tx)
- }
- })
- tx.state = "/etc/carlos/claim-activation/offline"
- write := func(path string, b []byte, mode os.FileMode) {
- t.Helper()
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(path, b, mode); err != nil {
- t.Fatal(err)
- }
- }
- write(offlineRebootState+"/carlos-cli", activationCarlosImage, 0755)
- write("/opt/carlos/carlos", activationCarlosImage, 0755)
- write("/opt/carlos/carlos.prev", activationCarlosImage, 0755)
- pub, key, err := ed25519.GenerateKey(nil)
- if err != nil {
- t.Fatal(err)
- }
- write(offlineRebootState+"/signing-key", key, 0600)
- signer := release.RootSigner(key)
- const bucket = "activation-seed-fixture"
- store := release.NewDirStore(offlineRebootState + "/bucket")
- rel := release.SystemRelease{Version: "v1.0.0", Source: "release:v1.0.0", ReleaseTag: "v1.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey("v1.0.0", "linux-arm64"), SHA256: hostfloor.Digest(activationCarlosImage)}}}
- if _, err := release.PromoteSystem(t.Context(), store, signer, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
- t.Fatal(err)
- }
- identity, _, err := store.Get(t.Context(), release.SystemPointerKey("router", "stable"))
- if err != nil {
- t.Fatal(err)
- }
- floor, _ := json.Marshal(hostfloor.Floor{Min: 1, MinRelease: "v1.0.0"})
- auth, err := release.SignRootEnvelope(signer, hostfloor.Authorization{Schema: 1, Purpose: "moves-protocol", DeploymentBucket: bucket, Key: hostfloor.Key, SHA256: hostfloor.Digest(floor)})
- if err != nil {
- t.Fatal(err)
- }
- policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: bucket, Mode: "armed", Floor: floor, Authorization: auth})
- write("/etc/carlos/host.env", []byte("CARLOS_HIBERNATE_BUCKET=activation-seed-fixture\nCARLOS_HIBERNATE_REGION=eu-west-1\nAWS_EC2_METADATA_DISABLED=true\nAWS_REGION=eu-west-1\nAWS_ACCESS_KEY_ID=fixture\nAWS_SECRET_ACCESS_KEY=fixture\nCARLOS_S3_ENDPOINT=http://127.0.0.1:1\nCARLOS_DEPLOYMENT_BUCKET="+bucket+"\nCARLOS_RELEASE_PUBKEY="+base64.StdEncoding.EncodeToString(pub)+"\n"), 0600)
- if err := (hostactors.Directory{Store: store, PublicKey: pub, Bucket: bucket}).Initialize(t.Context(), signer); err != nil {
- t.Fatal(err)
- }
- local := hostfloor.Enrollment{Box: "seed-fixture", Enrollment: strings.Repeat("a", 32), InstanceID: "i-fixture"}
- invitation, err := release.SignRootEnvelope(signer, hostactors.Invitation{V: 1, Bucket: bucket, Reference: local.Box, Enrollment: local.Enrollment, EvidenceSHA256: strings.Repeat("b", 64)})
- if err != nil {
- t.Fatal(err)
- }
- if _, err := store.Put(t.Context(), "control/host-actor-invitations/"+local.Box+".json", invitation, ""); err != nil {
- t.Fatal(err)
- }
- localRaw, _ := json.Marshal(local)
- write(offlineRebootState+"/enrollment.json", localRaw, 0600)
- write(offlineRebootState+"/policy.json", policy, 0600)
- write(offlineRebootState+"/identity.json", identity, 0600)
- // The real guard-arm installs the dependency-bearing drop-in. Its signed
- // roster invitation and loopback S3 endpoint are fixture authorization.
- if err := os.Mkdir(offlineRebootState+"/data", 0755); err != nil {
- t.Fatal(err)
- }
- endpoint := activationS3Fixture(t, store)
- arm := exec.CommandContext(t.Context(), offlineRebootState+"/carlos-cli", "system", "guard-arm", "--policy", offlineRebootState+"/policy.json", "--identity", offlineRebootState+"/identity.json", "--enrollment", offlineRebootState+"/enrollment.json")
- arm.Dir = offlineRebootState
- arm.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "HOME=" + offlineRebootState, "CARLOS_DEPLOYMENT_BUCKET=" + bucket, "CARLOS_RELEASE_PUBKEY=" + base64.StdEncoding.EncodeToString(pub), "CARLOS_DATA_DIR=" + offlineRebootState + "/data", "AWS_REGION=eu-west-1", "AWS_ACCESS_KEY_ID=fixture", "AWS_SECRET_ACCESS_KEY=fixture", "AWS_EC2_METADATA_DISABLED=true", "CARLOS_S3_ENDPOINT=" + endpoint}
- if out, err := arm.CombinedOutput(); err != nil {
- t.Fatal("guard-arm", string(out), err)
- }
- guard, err := os.ReadFile(filepath.Join(tx.units, "carlos-edge.service.d/90-host-floor.conf"))
- if err != nil || string(guard) != activationSeedGuardOriginal {
- t.Fatal("unexpected production guard", err)
- }
- servingRunnerSystemctl(t, "daemon-reload")
- servingRunnerSystemctl(t, "enable", "carlos-edge.service", "carlos-system-recover.service", "carlos-adopt.timer")
- servingRunnerSystemctl(t, "start", "carlos-edge.service")
- servingRunnerSystemctl(t, append([]string{"enable", "--now"}, activationTriggers...)...)
- servingRunnerSystemctl(t, "stop", "carlos-adopt.service")
- servingRunnerSystemctl(t, "reset-failed", "carlos-adopt.service")
- assertActivationProductionEdge(t)
- return tx
-}
-
-func TestRootActivationProductionRestore(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- tx.checkpoint = func(point string) error {
- if point == "replace-carlos-edge.service" || point == "start-carlos-edge.service" {
- if phase := offlineRecord(t, tx).Phase; phase != "complete" {
- t.Fatalf("edge released before closed: %s at %s", phase, point)
- }
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("guarded restore", err)
- }
- assertActivationProductionEdge(t)
-}
-
-func TestRootActivationProductionCompleteResume(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- stop := errors.New("complete interruption")
- tx.checkpoint = func(point string) error {
- if point == "journal-complete" {
- return stop
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
- t.Fatal(err)
- }
- tx.checkpoint = nil
- // An actual failed recovery unit models the state left by the previous gate.
- activationProductionWrite(t, "/etc/carlos/host.env.bad", []byte("bad"), 0600)
- if err := os.Rename("/etc/carlos/host.env", "/etc/carlos/host.env.saved"); err != nil {
- t.Fatal(err)
- }
- if _, err := tx.systemctl(t.Context(), "start", "carlos-system-recover.service"); err == nil {
- t.Fatal("failure injection did not fail")
- }
- if err := os.Rename("/etc/carlos/host.env.saved", "/etc/carlos/host.env"); err != nil {
- t.Fatal(err)
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("resume", err)
- }
- assertActivationProductionRecorded(t, tx, true)
-}
-
-func assertActivationProductionRecorded(t *testing.T, tx activationOffline, files bool) {
- t.Helper()
- for _, item := range offlineRecord(t, tx).Items {
- load, active, err := tx.status(t.Context(), item.Name)
- wantLoad, wantActive := "loaded", "inactive"
- if item.Masked {
- wantLoad = "masked"
- }
- if item.Active && !item.Masked {
- wantActive = "active"
- }
- if err != nil || load != wantLoad || active != wantActive {
- t.Errorf("%s recorded %s/%s got %s/%s: %v", item.Name, wantLoad, wantActive, load, active, err)
- }
- if files {
- if !sameActivationFile(filepath.Join(tx.units, item.Name), filepath.Join(tx.state, item.Name+".original")) {
- t.Errorf("original inode changed: %s", item.Name)
- }
- if err := tx.checkItem(item); err != nil {
- t.Errorf("original bytes or metadata changed: %s: %v", item.Name, err)
- }
- }
- }
-}
-
-func TestRootActivationProductionEdgeFailureClosed(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- bad := filepath.Join(tx.units, "carlos-edge.service.d/91-runner-failure.conf")
- activationProductionWrite(t, bad, []byte("[Service]\nExecStartPre=/usr/bin/false\nRestart=no\n"), 0644)
- if err := tx.restore(t.Context(), false); err == nil {
- t.Fatal("real failed edge start succeeded")
- }
- if r := offlineRecord(t, tx); r.Phase != "complete" || !r.EdgePending {
- t.Fatal("edge failure reopened transaction", r.Phase, r.EdgePending)
- }
- if err := activationAdoptionAllowed("/etc"); err == nil {
- t.Fatal("failed edge released adoption", err)
- }
- if err := os.Remove(bad); err != nil {
- t.Fatal(err)
- }
- servingRunnerSystemctl(t, "daemon-reload")
- servingRunnerSystemctl(t, "reset-failed", "carlos-edge.service")
- servingRunnerSystemctl(t, "start", "carlos-edge.service")
- assertActivationProductionEdge(t)
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("ordinary edge retry", err)
- }
-}
-
-func TestRootActivationProductionRecoveryGate(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- stop := errors.New("complete")
- tx.checkpoint = func(point string) error {
- if point == "journal-complete" {
- return stop
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
- t.Fatal(err)
- }
- if err := activationAdoptionAllowed("/etc"); err == nil {
- t.Fatal("complete allowed adoption")
- }
- if err := (&systemApplier{etcDir: "/etc"}).Run(t.Context(), false); !errors.Is(err, errActivation) {
- t.Fatal("complete allowed update", err)
- }
- if _, err := tx.systemctl(t.Context(), "start", "carlos-system-recover.service"); err != nil {
- t.Fatal("settled recovery no-op refused", err)
- }
- raw, err := tx.systemctl(t.Context(), "show", "carlos-system-recover.service", "--property=Result", "--value")
- if err != nil || strings.TrimSpace(string(raw)) != "success" {
- t.Fatal("complete did not exclude real recovery", string(raw), err)
- }
- journal := filepath.Join(tx.updateDir(), "journal.json")
- activationProductionWrite(t, journal, []byte("pending"), 0600)
- if err := (&systemApplier{etcDir: "/etc"}).Run(t.Context(), true); err == nil {
- t.Fatal("pending recovery passed the no-op gate")
- }
- if raw, err := os.ReadFile(journal); err != nil || string(raw) != "pending" {
- t.Fatal("pending recovery changed evidence", err)
- }
- if err := os.Remove(journal); err != nil {
- t.Fatal(err)
- }
- tx.checkpoint = nil
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- assertActivationProductionEdge(t)
-}
-
-func TestRootActivationProductionRestoreBoundaries(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- for cut := 1; cut < 100; cut++ {
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(cut, err)
- }
- count := 0
- point := ""
- stop := errors.New("interruption")
- tx.checkpoint = func(name string) error {
- count++
- r := offlineRecord(t, tx)
- if r.Phase != "done" && r.Phase != "complete" {
- target, _ := os.Readlink(filepath.Join(tx.units, activationEdgeUnit))
- if target != "/dev/null" {
- t.Fatal("edge released before closure", name, r.Phase)
- }
- }
- if count == cut {
- point = name
- return stop
- }
- return nil
- }
- err := tx.restore(t.Context(), false)
- if err != nil && !errors.Is(err, stop) {
- t.Fatal(cut, point, err)
- }
- tx.checkpoint = nil
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal("resume", cut, point, err)
- }
- assertActivationProductionRecorded(t, tx, true)
- assertActivationProductionEdge(t)
- t.Log("production restore boundary passed", cut, point)
- if err == nil {
- return
- }
- if err := os.RemoveAll(tx.state); err != nil {
- t.Fatal(err)
- }
- }
- t.Fatal("boundary loop did not complete")
-}
-
-func activationProductionUser(t *testing.T) (int, int) {
- t.Helper()
- if os.Geteuid() != 0 {
- t.Fatal("root required")
- }
- u, err := user.Lookup(serviceUser)
- if _, unknown := err.(user.UnknownUserError); unknown {
- if _, err := os.Lstat("/etc/carlos-activation-runner-user"); !os.IsNotExist(err) {
- t.Fatal("existing fixture user marker", err)
- }
- if _, err := user.LookupGroup(serviceUser); err == nil {
- t.Fatal("pre-existing group without user")
- }
- if out, err := exec.Command("useradd", "--system", "--user-group", "--no-create-home", "--shell", "/usr/sbin/nologin", serviceUser).CombinedOutput(); err != nil {
- t.Fatal(string(out), err)
- }
- activationProductionWrite(t, "/etc/carlos-activation-runner-user", nil, 0600)
- u, err = user.Lookup(serviceUser)
- }
- if err != nil {
- t.Fatal(err)
- }
- uid, err := strconv.Atoi(u.Uid)
- if err != nil {
- t.Fatal(err)
- }
- gid, err := strconv.Atoi(u.Gid)
- if err != nil {
- t.Fatal(err)
- }
- return uid, gid
-}
-
-func TestRootActivationProductionOperatorMask(t *testing.T) {
- tx := setupActivationProduction(t)
- defer cleanupActivationProduction(t, tx)
- path := filepath.Join(tx.units, "carlos-stage.service")
- if err := os.Rename(path, offlineRebootState+"/stage.original"); err != nil {
- t.Fatal(err)
- }
- if err := os.Symlink("/dev/null", path); err != nil {
- t.Fatal(err)
- }
- servingRunnerSystemctl(t, "daemon-reload")
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- if err := verifyOfflineRootMigration(t.Context()); err != nil {
- t.Fatal(err)
- }
- edge := filepath.Join(tx.units, activationEdgeUnit)
- if err := os.Remove(edge); err != nil {
- t.Fatal(err)
- }
- if err := os.Symlink("/dev/null", edge); err != nil {
- t.Fatal(err)
- }
- if err := tx.restore(t.Context(), false); err == nil {
- t.Fatal("unowned edge replacement accepted")
- }
- if err := os.Remove(edge); err != nil {
- t.Fatal(err)
- }
- if err := os.Link(filepath.Join(tx.state, activationEdgeUnit+".mask"), edge); err != nil {
- t.Fatal(err)
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- assertActivationProductionRecorded(t, tx, true)
- assertActivationProductionEdge(t)
-}
diff --git a/internal/host/activation_restore_order_rootrunner_linux_test.go b/internal/host/activation_restore_order_rootrunner_linux_test.go
deleted file mode 100644
index a2325960a..000000000
--- a/internal/host/activation_restore_order_rootrunner_linux_test.go
+++ /dev/null
@@ -1,245 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
-)
-
-func offlineRecord(t *testing.T, tx activationOffline) activationOfflineRecord {
- t.Helper()
- raw, err := os.ReadFile(filepath.Join(tx.state, "transaction.json"))
- if err != nil {
- t.Fatal(err)
- }
- var r activationOfflineRecord
- if err := json.Unmarshal(raw, &r); err != nil {
- t.Fatal(err)
- }
- return r
-}
-
-func TestRootActivationModelRestoreEveryBoundaryBoot(t *testing.T) {
- for cut := 1; cut < 160; cut++ {
- reached := false
- t.Run(fmt.Sprint(cut), func(t *testing.T) {
- tx, _ := offlineFixture(t)
- base := tx.systemctl
- active := map[string]bool{"carlos-edge.service": true}
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- switch args[0] {
- case "start":
- active[args[1]] = true
- case "stop":
- active[args[1]] = false
- }
- raw, err := base(ctx, args...)
- if args[0] == "show" && active[args[1]] {
- raw = []byte(strings.ReplaceAll(string(raw), "ActiveState=inactive", "ActiveState=active"))
- }
- return raw, err
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- count := 0
- stop := errors.New("power loss")
- point := ""
- tx.checkpoint = func(name string) error {
- count++
- r := offlineRecord(t, tx)
- if r.Phase != "complete" && r.Phase != "done" {
- if target, _ := os.Readlink(filepath.Join(tx.units, "carlos-edge.service")); target != "/dev/null" {
- t.Fatalf("edge unmasked before complete at %s", name)
- }
- }
- if count == cut {
- reached = true
- point = name
- return stop
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); err != nil && !errors.Is(err, stop) {
- t.Fatal(err)
- }
- tx.checkpoint = nil
- // Model boot from persistent masks, including an enabled but inactive timer.
- for _, name := range activationOfflineUnits() {
- target, _ := os.Readlink(filepath.Join(tx.units, name))
- active[name] = target != "/dev/null" && (name == "carlos-adopt.timer" || name == "carlos-edge.service")
- }
- r := offlineRecord(t, tx)
- if r.Phase != "complete" && r.Phase != "done" && active["carlos-edge.service"] {
- t.Fatalf("boot served at %s", point)
- }
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatalf("resume at %s: %v", point, err)
- }
- if !active["carlos-edge.service"] {
- t.Fatal("edge not restored")
- }
- if !active["carlos-adopt.timer"] {
- t.Fatalf("inactive timer not reconciled after %s", point)
- }
- })
- if !reached {
- break
- }
- }
-}
-
-func TestRootActivationModelRepairExclusion(t *testing.T) {
- tx, d := offlineFixture(t)
- etc := filepath.Join(d, "etc")
- units := filepath.Join(etc, "systemd", "system")
- if err := os.MkdirAll(filepath.Dir(units), 0755); err != nil {
- t.Fatal(err)
- }
- if err := os.Rename(tx.units, units); err != nil {
- t.Fatal(err)
- }
- tx.units = units
- tx.state = filepath.Join(etc, "carlos", "claim-activation", "offline")
- active := map[string]bool{"carlos-adopt.timer": true, "carlos-edge.service": true}
- for _, name := range activationTriggers {
- active[name] = true
- }
- tx.systemctl = func(ctx context.Context, args ...string) ([]byte, error) {
- switch args[0] {
- case "stop":
- active[args[1]] = false
- case "start":
- active[args[1]] = true
- case "show":
- load, state := "loaded", "inactive"
- if target, _ := os.Readlink(filepath.Join(tx.units, args[1])); target == "/dev/null" {
- load = "masked"
- }
- if active[args[1]] {
- state = "active"
- }
- return []byte("LoadState=" + load + "\nActiveState=" + state + "\n"), nil
- }
- return nil, nil
- }
- if err := tx.begin(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- stop := errors.New("lost adoption timer start reply")
- tx.checkpoint = func(name string) error {
- if name == "verify-close" {
- return stop
- }
- return nil
- }
- if err := tx.restore(t.Context(), false); !errors.Is(err, stop) {
- t.Fatal(err)
- }
- path := filepath.Join(tx.units, "carlos-system-update.path")
- before, err := os.Lstat(path)
- if err != nil {
- t.Fatal(err)
- }
- calls := 0
- ctl := func(args ...string) (string, error) { calls++; return "", nil }
- probe := func(args ...string) (string, error) { return "inactive", nil }
- ensureSystemUnits(etc, filepath.Join(d, "apply.lock"), ctl, probe, t.Logf)
- after, err := os.Lstat(path)
- if err != nil {
- t.Fatal(err)
- }
- if !os.SameFile(before, after) || calls != 0 {
- t.Fatalf("real ensureSystemUnits ran during interrupted restore: same inode=%v commands=%d", os.SameFile(before, after), calls)
- }
- tx.checkpoint = nil
- if err := tx.restore(t.Context(), false); err != nil {
- t.Fatal(err)
- }
- ensureSystemUnits(etc, filepath.Join(d, "apply.lock"), ctl, probe, t.Logf)
- after, err = os.Lstat(path)
- if err != nil {
- t.Fatal(err)
- }
- if os.SameFile(before, after) || calls == 0 {
- t.Fatal("real ensureSystemUnits did not resume after restore")
- }
-}
-
-func TestRootActivationAdoptionGate(t *testing.T) {
- d := activationRootFixture(t)
- etc := filepath.Join(d, "etc")
- if err := os.Mkdir(etc, 0755); err != nil {
- t.Fatal(err)
- }
- if err := activationAdoptionAllowed(etc); err != nil {
- t.Fatal("absent transaction changed adoption", err)
- }
- if _, err := os.Lstat(filepath.Join(etc, "carlos")); !os.IsNotExist(err) {
- t.Fatal("absent gate wrote state", err)
- }
- tx := activationOffline{units: filepath.Join(etc, "systemd", "system"), state: filepath.Join(etc, "carlos", "claim-activation", "offline")}
- state, err := activationDir(tx.state, true)
- if err != nil {
- t.Fatal(err)
- }
- defer state.Close()
- if err := activationAdoptionAllowed(etc); err == nil {
- t.Fatal("missing journal permitted adoption")
- }
- r := activationOfflineRecord{V: 1, Units: tx.units}
- for _, name := range activationOfflineUnits() {
- r.Items = append(r.Items, activationOfflineItem{Name: name})
- }
- for _, phase := range []string{"prepared", "held", "seed-prepared", "restoring", "released", "complete", "done"} {
- r.Phase = phase
- if err := tx.save(state, r); err != nil {
- t.Fatal(err)
- }
- err := activationAdoptionAllowed(etc)
- if (err == nil) != (phase == "done") {
- t.Fatal("gate phase", phase, err)
- }
- err = activationTransactionAllowed(etc, true)
- if (err == nil) != (phase == "done") || (errors.Is(err, errActivationRecoveryHeld) != (phase == "complete")) {
- t.Fatal("recovery gate phase", phase, err)
- }
- }
- if err := os.Chmod(tx.state, 0777); err != nil {
- t.Fatal(err)
- }
- if err := activationAdoptionAllowed(etc); err == nil {
- t.Fatal("writable state permitted adoption")
- }
- if err := os.Chmod(tx.state, 0700); err != nil {
- t.Fatal(err)
- }
- if err := os.Rename(tx.state, tx.state+".original"); err != nil {
- t.Fatal(err)
- }
- if err := os.Symlink(tx.state+".original", tx.state); err != nil {
- t.Fatal(err)
- }
- if err := activationAdoptionAllowed(etc); err == nil {
- t.Fatal("symlink state permitted adoption")
- }
- if err := os.Remove(tx.state); err != nil {
- t.Fatal(err)
- }
- if err := os.Rename(tx.state+".original", tx.state); err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(filepath.Join(tx.state, "transaction.json"), []byte("{}"), 0600); err != nil {
- t.Fatal(err)
- }
- if err := activationAdoptionAllowed(etc); err == nil {
- t.Fatal("invalid journal permitted adoption")
- }
-}
diff --git a/internal/host/activation_seed_cli_rootrunner_linux_test.go b/internal/host/activation_seed_cli_rootrunner_linux_test.go
deleted file mode 100644
index 5e940f8d8..000000000
--- a/internal/host/activation_seed_cli_rootrunner_linux_test.go
+++ /dev/null
@@ -1,263 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "bytes"
- "crypto/ed25519"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "syscall"
- "testing"
- "time"
-
- "github.com/carlosframework/platform/internal/hostfloor"
- "github.com/carlosframework/platform/internal/release"
-)
-
-// The operator CLI is the real built binary. The signed seed payload is a
-// local fixture: it accepts the exact argv, can block for interruption, and
-// never contacts a deployment. Files-seed data effects have separate witnesses.
-const activationSeedCLIPayload = `#!/bin/sh
-set -eu
-if test "$*" != 'system files seed'; then
- exec /etc/carlos-activation-rootrunner/carlos-cli "$@"
-fi
-test ! -f /data/activation-payload-fail
-echo started > /data/activation-payload-started
-while test -f /data/activation-payload-wait; do sleep 0.1; done
-exit 0
-`
-
-func setupActivationSeedCLI(t *testing.T) activationOffline {
- t.Helper()
- tx := setupActivationProduction(t)
- activationSeedCLI(t, "offline-begin")
- servingRunnerSystemctl(t, "start", "carlos-instance-control.service")
- raw, err := os.ReadFile("/etc/carlos/host.env")
- if err != nil {
- t.Fatal(err)
- }
- activationProductionWrite(t, "/etc/carlos/host.env", append(raw, []byte("CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM_ORIGINS=1\n")...), 0600)
- key, err := os.ReadFile(offlineRebootState + "/signing-key")
- if err != nil {
- t.Fatal(err)
- }
- signer := release.RootSigner(ed25519.PrivateKey(key))
- store := release.NewDirStore(offlineRebootState + "/bucket")
- rel := release.SystemRelease{Version: "v1.0.1", Source: "release:v1.0.1", ReleaseTag: "v1.0.1", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey("v1.0.1", "linux-arm64"), SHA256: hostfloor.Digest([]byte(activationSeedCLIPayload))}}}
- if _, err := release.PromoteSystem(t.Context(), store, signer, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
- t.Fatal(err)
- }
- identity, _, err := store.Get(t.Context(), release.SystemPointerKey("router", "stable"))
- if err != nil {
- t.Fatal(err)
- }
- activationProductionWrite(t, "/opt/carlos/system/guard/identities/v1.0.1.json", identity, 0644)
- activationProductionWrite(t, "/opt/carlos/carlos", []byte(activationSeedCLIPayload), 0755)
- return tx
-}
-
-func activationSeedPrepareRestore(t *testing.T) {
- t.Helper()
- activationProductionWrite(t, "/opt/carlos/carlos", activationCarlosImage, 0755)
- raw, err := os.ReadFile("/etc/carlos/host.env")
- if err != nil {
- t.Fatal(err)
- }
- raw = bytes.ReplaceAll(raw, []byte("CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM_ORIGINS=1\n"), nil)
- activationProductionWrite(t, "/etc/carlos/host.env", raw, 0600)
- servingRunnerSystemctl(t, "stop", "carlos-instance-control.service")
-}
-func activationSeedCLICommand(t *testing.T, args ...string) *exec.Cmd {
- t.Helper()
- cmd := exec.CommandContext(t.Context(), offlineRebootState+"/carlos-cli", append([]string{"system", "claims"}, args...)...)
- cmd.Dir = offlineRebootState
- cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "HOME=" + offlineRebootState, "AWS_EC2_METADATA_DISABLED=true"}
- return cmd
-}
-func activationSeedCLI(t *testing.T, args ...string) {
- t.Helper()
- out, err := activationSeedCLICommand(t, args...).CombinedOutput()
- if err != nil {
- t.Fatal(args, string(out), err)
- }
-}
-func cleanupActivationSeedCLI(t *testing.T, tx activationOffline) {
- cleanupActivationProduction(t, tx)
-}
-func assertActivationSeedCLIHeld(t *testing.T, tx activationOffline) {
- t.Helper()
- load, active, err := tx.status(t.Context(), "carlos-edge.service")
- if err != nil || load != "masked" || active != "inactive" {
- t.Fatal("edge not held", load, active, err)
- }
- guard, err := os.ReadFile(filepath.Join(tx.units, "carlos-edge.service.d/90-host-floor.conf"))
- if err != nil || string(guard) != activationSeedGuardOriginal {
- t.Fatal("guard bytes not restored", err)
- }
- state, err := activationDir(tx.state, false)
- if err != nil {
- t.Fatal(err)
- }
- defer state.Close()
- r, err := tx.read(state)
- if err != nil || r.Phase != "held" || r.SeedGuard == nil {
- t.Fatal("seed journal", r.Phase, err)
- }
- g := activationOffline{units: filepath.Join(tx.units, "carlos-edge.service.d"), state: tx.state}
- if err := g.checkItem(*r.SeedGuard); err != nil {
- t.Fatal("guard metadata/identity changed", err)
- }
-}
-func startActivationSeedCLIBlocked(t *testing.T) *exec.Cmd {
- t.Helper()
- if err := os.WriteFile("/data/activation-payload-wait", nil, 0600); err != nil {
- t.Fatal(err)
- }
- os.Remove("/data/activation-payload-started")
- cmd := activationSeedCLICommand(t, "offline-seed")
- var output bytes.Buffer
- cmd.Stdout, cmd.Stderr = &output, &output
- if err := cmd.Start(); err != nil {
- t.Fatal(err)
- }
- deadline := time.Now().Add(20 * time.Second)
- for time.Now().Before(deadline) {
- if _, err := os.Stat("/data/activation-payload-started"); err == nil {
- return cmd
- }
- time.Sleep(50 * time.Millisecond)
- }
- cmd.Process.Kill()
- cmd.Wait()
- t.Fatal("seed payload did not start", output.String())
- return nil
-}
-func TestRootActivationSeedCLI(t *testing.T) {
- tx := setupActivationSeedCLI(t)
- defer cleanupActivationSeedCLI(t, tx)
- activationSeedCLI(t, "offline-seed", "--dry-run")
- activationSeedCLI(t, "offline-seed")
- assertActivationSeedCLIHeld(t, tx)
- result, err := os.ReadFile(filepath.Join(tx.state, "seed-result.txt"))
- if err != nil || !bytes.Contains(result, []byte("ExecMainStatus=0")) {
- t.Fatal("missing successful seed", string(result), err)
- }
- interruptActivationSeedCLI(t, tx)
- activationSeedPrepareRestore(t)
- activationSeedCLI(t, "offline-restore")
-}
-func interruptActivationSeedCLI(t *testing.T, tx activationOffline) {
- t.Helper()
- cmd := startActivationSeedCLIBlocked(t)
- if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
- t.Fatal(err)
- }
- if err := cmd.Wait(); err == nil {
- t.Fatal("interrupted CLI succeeded")
- }
- load, active, err := tx.status(t.Context(), "carlos-edge.service")
- if err != nil || load != "masked" || active != "inactive" && active != "failed" {
- t.Fatal("cancel did not remask", load, active, err)
- }
- os.Remove("/data/activation-payload-wait")
- activationSeedCLI(t, "offline-seed")
- assertActivationSeedCLIHeld(t, tx)
-}
-func TestRootRebootActivationSeedCLIPhase1(t *testing.T) {
- for _, test := range []struct {
- name string
- run func(*testing.T)
- }{
- {"guarded-restore", TestRootActivationProductionRestore},
- {"complete-resume", TestRootActivationProductionCompleteResume},
- {"recovery-gate", TestRootActivationProductionRecoveryGate},
- {"ordinary-edge-failure", TestRootActivationProductionEdgeFailureClosed},
- {"operator-mask", TestRootActivationProductionOperatorMask},
- {"restore-boundaries", TestRootActivationProductionRestoreBoundaries},
- {"seed-cli", TestRootActivationSeedCLI},
- {"seed-boundaries", TestRootActivationSeedInterruptions},
- {"seed-refusals", TestRootActivationSeedProductionFailures},
- {"gate-shapes", TestRootActivationAdoptionGate},
- {"model-offline", TestRootActivationModelOfflineInterruptions},
- {"model-ownership", TestRootActivationModelReplacementRefused},
- {"model-edge-order", TestRootActivationModelEdgeLast},
- {"model-trigger-ownership", TestRootActivationModelReleasedTriggerOwnership},
- {"model-boot", TestRootActivationModelRestoreEveryBoundaryBoot},
- {"model-repair", TestRootActivationModelRepairExclusion},
- } {
- t.Run(test.name, test.run)
- }
- if t.Failed() {
- return
- }
-
- tx := setupActivationSeedCLI(t)
- cmd := startActivationSeedCLIBlocked(t)
- if err := cmd.Process.Kill(); err != nil {
- t.Fatal(err)
- }
- if err := cmd.Wait(); err == nil {
- t.Fatal("killed CLI succeeded")
- }
- load, err := tx.systemctl(t.Context(), "show", "carlos-edge.service", "--property=LoadState", "--value")
- if err != nil || strings.TrimSpace(string(load)) != "loaded" {
- t.Fatal("crash did not leave unmask window", string(load), err)
- }
- guard, err := os.ReadFile(filepath.Join(tx.units, "carlos-edge.service.d/90-host-floor.conf"))
- if err != nil || string(guard) != activationSeedGuardReplacement {
- t.Fatal("dependency replacement not durable", err)
- }
- invocation, err := tx.systemctl(t.Context(), "show", "carlos-edge.service", "--property=InvocationID", "--value")
- if err != nil || strings.TrimSpace(string(invocation)) == "" {
- t.Fatal("missing pre-reboot invocation", err)
- }
- if err := os.WriteFile(offlineRebootState+"/invocation", invocation, 0600); err != nil {
- t.Fatal(err)
- }
- boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
- if err != nil {
- t.Fatal(err)
- }
- if err := os.WriteFile(offlineRebootState+"/boot", boot, 0600); err != nil {
- t.Fatal(err)
- }
-}
-func TestRootRebootActivationSeedCLIPhase2(t *testing.T) {
- tx := activationOffline{units: "/etc/systemd/system", state: "/etc/carlos/claim-activation/offline", systemctl: offlineRealSystemctl}
- defer cleanupActivationSeedCLI(t, tx)
- before, err := os.ReadFile(offlineRebootState + "/boot")
- if err != nil {
- t.Fatal(err)
- }
- after, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
- if err != nil || bytes.Equal(before, after) {
- t.Fatal("no actual reboot", err)
- }
- unit, err := tx.systemctl(t.Context(), "show", "carlos-edge.service", "--property=Type,ExecStart,Requires")
- if err != nil || !strings.Contains(string(unit), "Type=oneshot") || !strings.Contains(string(unit), "/opt/carlos/carlos system files seed") || strings.Contains(string(unit), "carlos-system-recover.service") {
- t.Fatal("wrong reboot command", string(unit), err)
- }
- previous, err := os.ReadFile(offlineRebootState + "/invocation")
- if err != nil {
- t.Fatal(err)
- }
- current, err := tx.systemctl(t.Context(), "show", "carlos-edge.service", "--property=InvocationID", "--value")
- if err != nil || strings.TrimSpace(string(current)) == "" || bytes.Equal(previous, current) {
- t.Fatal("no new seed invocation after reboot", string(current), err)
- }
- pid, err := filesSeedEdgePID(t.Context())
- if err != nil || pid <= 0 {
- t.Fatal("seed not running after reboot", pid, err)
- }
- t.Log("reboot started a new seed invocation", strings.TrimSpace(string(current)), "pid", pid)
- os.Remove("/data/activation-payload-wait")
- activationSeedCLI(t, "offline-seed")
- assertActivationSeedCLIHeld(t, tx)
- interruptActivationSeedCLI(t, tx)
- activationSeedPrepareRestore(t)
- activationSeedCLI(t, "offline-restore")
-}
diff --git a/internal/host/activation_seed_linux.go b/internal/host/activation_seed_linux.go
deleted file mode 100644
index 88bdcd768..000000000
--- a/internal/host/activation_seed_linux.go
+++ /dev/null
@@ -1,455 +0,0 @@
-//go:build linux
-
-package host
-
-import (
- "bytes"
- "context"
- "errors"
- "io"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-
- "github.com/carlosframework/platform/internal/hostfloor"
- "github.com/carlosframework/platform/internal/release"
- "github.com/carlosframework/platform/internal/safefile"
- "golang.org/x/sys/unix"
-)
-
-const activationSeedDropIn = "95-claim-activation-seed.conf"
-const activationSeedUnit = `[Unit]
-Requires=carlos-instance-control.service
-After=carlos-instance-control.service
-OnSuccess=
-OnFailure=
-[Service]
-Type=oneshot
-RemainAfterExit=yes
-ExecCondition=
-ExecStartPre=
-ExecStart=
-ExecStart=/opt/carlos/carlos system files seed
-ExecStartPost=
-ExecReload=
-ExecStop=
-ExecStopPost=
-Restart=no
-TimeoutStartSec=0
-TimeoutStopSec=45s
-`
-
-func activationSeedPreflight(ctx context.Context) error {
- raw, err := activationRead("/etc/carlos/host.env", false)
- if err != nil {
- return err
- }
- env, err := activationEnvironment(raw)
- if err != nil {
- return err
- }
- if env["CARLOS_HOST_CLAIM"] == "" || env["CARLOS_HOST_CLAIM_ORIGINS"] != "1" || env["CARLOS_MOVES_PHASE1"] != "" || env["CARLOS_REGION"] != "" {
- return errActivation
- }
- pub, err := release.ParsePublicKey(env["CARLOS_RELEASE_PUBKEY"], "CARLOS_RELEASE_PUBKEY")
- if err != nil {
- return errActivation
- }
- d := hostfloor.Disk{Dir: "/opt/carlos/system/guard", Bucket: env["CARLOS_DEPLOYMENT_BUCKET"], PublicKey: pub}
- if policy, _, err := d.Load(); err != nil || policy.Mode == "unpublished" {
- return errActivation
- }
- f, err := hostfloor.OpenProtected("/opt/carlos/carlos")
- if err != nil {
- return err
- }
- defer f.Close()
- if _, err = d.VerifyFile(f, runtime.GOOS+"-"+runtime.GOARCH); err != nil {
- return err
- }
- raw, err = claimSystemctl(ctx, "is-active", "carlos-instance-control.service")
- if err != nil || strings.TrimSpace(string(raw)) != "active" {
- return errActivation
- }
- return verifyOfflineRootMigration(ctx)
-}
-
-// The persistent override is installed before the edge mask is lifted and
-// removed only after that mask is durable again. A reboot during the window
-// can therefore run only seed, never the normal serving agent.
-func (t activationOffline) seed(ctx context.Context, content []byte, dry bool) (resultErr error) {
- units, state, err := t.dirs(false)
- if err != nil {
- return err
- }
- defer units.Close()
- defer state.Close()
- if !dry {
- lock, e := activationLock(state)
- if e != nil {
- return e
- }
- defer lock.Close()
- }
- r, err := t.read(state)
- if err != nil {
- return err
- }
- if r.Phase != "held" && r.Phase != "seed-prepared" {
- return errActivation
- }
- edge := r.Items[len(r.Items)-1]
- if edge.Name != "carlos-edge.service" || edge.Masked {
- return errActivation
- }
- // Cleanup also covers reload, verification and cancellation failures. A killed
- // process leaves the durable seed configuration for the next invocation.
- if !dry {
- defer func() {
- if resultErr == nil {
- return
- }
- cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Minute)
- defer cancel()
- resultErr = errors.Join(resultErr, t.seedRemask(cleanup, units, state, edge))
- }()
- }
- if !dry && r.Phase == "seed-prepared" {
- if err = t.seedRemask(ctx, units, state, edge); err != nil {
- return err
- }
- }
- if t.seedPreflight != nil {
- if err = t.seedPreflight(ctx); err != nil {
- return err
- }
- }
- for _, item := range r.Items {
- if err = t.checkItem(item); err != nil {
- return err
- }
- if item.Name != edge.Name {
- load, active, e := t.status(ctx, item.Name)
- if e != nil {
- return e
- }
- if load != "masked" || active != "inactive" && active != "failed" {
- return errActivation
- }
- }
- }
- dropPath := filepath.Join(t.units, edge.Name+".d")
- drop, err := activationDir(dropPath, false)
- exists := false
- if err == nil {
- old, e := activationReadAt(drop, activationSeedDropIn, false)
- exists = e == nil
- if e != nil && !errors.Is(e, os.ErrNotExist) {
- drop.Close()
- return e
- }
- if exists && (r.Phase == "held" || !bytes.Equal(old, content)) {
- drop.Close()
- return errActivation
- }
- drop.Close()
- } else if !errors.Is(err, os.ErrNotExist) {
- return err
- }
- masked := sameActivationFile(filepath.Join(t.units, edge.Name), filepath.Join(t.state, edge.Name+".mask"))
- if !masked && (r.Phase != "seed-prepared" || !exists) {
- return errActivation
- }
- if err = activationSeedDropIns(t.units); err != nil {
- return err
- }
- guard, err := t.seedGuard(state, &r, dry)
- if err != nil {
- return err
- }
- if guard != nil {
- defer guard.Close()
- }
- if dry {
- return nil
- }
- if r.Phase == "held" {
- r.Phase = "seed-prepared"
- if err = t.save(state, r); err != nil {
- return err
- }
- }
- if guard != nil {
- if err = t.seedGuardReplace(guard, state, r, false); err != nil {
- return err
- }
- if err = t.point("seed-guard-replaced"); err != nil {
- return err
- }
- }
- drop, err = activationDir(dropPath, true)
- if err != nil {
- return err
- }
- defer drop.Close()
- if err = safefile.WriteAt(int(drop.Fd()), activationSeedDropIn, content, 0644); err != nil {
- return err
- }
- if err = t.point("seed-override"); err != nil {
- return err
- }
- if err = t.replace(units, state, edge, true); err != nil {
- return err
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- if err = t.point("seed-reload"); err != nil {
- return err
- }
- effective, err := t.systemctl(ctx, "show", edge.Name, "--property=ExecStart,Type,RemainAfterExit,Restart,Requires")
- if err != nil {
- return err
- }
- command := ""
- for _, line := range strings.Split(string(content), "\n") {
- if strings.HasPrefix(line, "ExecStart=") {
- command = strings.TrimPrefix(line, "ExecStart=")
- }
- }
- fields := strings.Fields(command)
- if len(fields) == 0 || !strings.Contains(string(effective), "ExecStart={ path="+fields[0]+" ; argv[]="+command+" ;") || strings.Count(string(effective), "argv[]=") != 1 || !strings.Contains(string(effective), "Type=oneshot\n") || !strings.Contains(string(effective), "RemainAfterExit=yes\n") || !strings.Contains(string(effective), "Restart=no\n") || strings.Contains(string(effective), "carlos-system-recover.service") {
- return errActivation
- }
- // The caller retains a failed seed's result and retries deliberately. Even
- // if start fails, stop and restore the mask before deleting the override.
- _, startErr := t.systemctl(ctx, "start", edge.Name)
- cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Minute)
- defer cancel()
- ctx = cleanup
- result, showErr := t.systemctl(ctx, "show", edge.Name, "--property=InvocationID,Result,ExecMainStatus,ActiveState,SubState")
- if showErr == nil {
- showErr = safefile.WriteAt(int(state.Fd()), "seed-result.txt", result, 0600)
- }
- if err = t.point("seed-started"); err != nil {
- return err
- }
- if _, err = t.systemctl(ctx, "stop", edge.Name); err != nil {
- return errors.Join(startErr, showErr, err)
- }
- if err = t.point("seed-stopped"); err != nil {
- return err
- }
- if err = t.replace(units, state, edge, false); err != nil {
- return err
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- load, active, err := t.status(ctx, edge.Name)
- if err != nil || load != "masked" || active != "inactive" && active != "failed" {
- return errActivation
- }
- if err = t.point("seed-remasked"); err != nil {
- return err
- }
- if guard != nil {
- if err = t.seedGuardReplace(guard, state, r, true); err != nil {
- return err
- }
- if err = t.point("seed-guard-restored"); err != nil {
- return err
- }
- }
- old, err := activationReadAt(drop, activationSeedDropIn, false)
- if err != nil || !bytes.Equal(old, content) {
- return errActivation
- }
- if err = unix.Unlinkat(int(drop.Fd()), activationSeedDropIn, 0); err != nil {
- return err
- }
- if err = drop.Sync(); err != nil {
- return err
- }
- if err = t.point("seed-removed"); err != nil {
- return err
- }
- if _, err = t.systemctl(ctx, "daemon-reload"); err != nil {
- return err
- }
- r.Phase = "held"
- if err = t.save(state, r); err != nil {
- return err
- }
- return errors.Join(startErr, showErr)
-}
-
-// A later drop-in could replace the seed command at boot before the loaded
-// unit can be checked. Refuse that shape before removing the persistent mask.
-func activationSeedDropIns(units string) error {
- roots := []string{units, "/etc/systemd/system.control", "/run/systemd/system.control", "/run/systemd/transient", "/run/systemd/generator.early", "/run/systemd/system", "/usr/local/lib/systemd/system", "/usr/lib/systemd/system", "/run/systemd/generator", "/run/systemd/generator.late"}
- for _, root := range roots {
- for _, name := range []string{"service.d", "carlos-.service.d", "carlos-edge.service.d"} {
- dir, err := activationDir(filepath.Join(root, name), false)
- if errors.Is(err, os.ErrNotExist) {
- continue
- }
- if err != nil {
- return err
- }
- entries, err := dir.ReadDir(-1)
- if err != nil {
- dir.Close()
- return err
- }
- for _, entry := range entries {
- if !strings.HasSuffix(entry.Name(), ".conf") {
- continue
- }
- if entry.Name() > activationSeedDropIn {
- dir.Close()
- return errActivation
- }
- if _, err := activationSeedReadAt(dir, entry.Name()); err != nil {
- dir.Close()
- return err
- }
- }
- dir.Close()
- }
- }
- return nil
-}
-
-// This is the exact guard-arm edge configuration. Only its recovery dependency
-// is suspended; the normal guard command remains until the seed override wins.
-const activationSeedGuardOriginal = "[Unit]\nRequires=carlos-system-recover.service\nAfter=carlos-system-recover.service\n[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec edge\n"
-const activationSeedGuardReplacement = "[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec edge\n"
-
-func (t activationOffline) seedRemask(ctx context.Context, units, state *os.File, edge activationOfflineItem) error {
- // Validate ownership before stopping anything. Persist the mask even when
- // stop fails, so a reboot cannot start the serving command.
- if err := t.checkItem(edge); err != nil {
- return err
- }
- _, stopErr := t.systemctl(ctx, "stop", edge.Name)
- maskErr := t.replace(units, state, edge, false)
- _, reloadErr := t.systemctl(ctx, "daemon-reload")
- load, active, statusErr := t.status(ctx, edge.Name)
- if statusErr == nil && (load != "masked" || active != "inactive" && active != "failed") {
- statusErr = errActivation
- }
- return errors.Join(stopErr, maskErr, reloadErr, statusErr)
-}
-
-func (t activationOffline) seedGuard(state *os.File, r *activationOfflineRecord, dry bool) (*os.File, error) {
- dir, err := activationDir(filepath.Join(t.units, "carlos-edge.service.d"), false)
- if errors.Is(err, os.ErrNotExist) && r.SeedGuard == nil {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- fail := func(err error) (*os.File, error) { dir.Close(); return nil, err }
- const name = "90-host-floor.conf"
- if r.Phase == "seed-prepared" && r.SeedGuard != nil {
- if err := t.seedGuardCheck(r); err != nil {
- return fail(err)
- }
- return dir, nil
- }
- raw, err := activationSeedReadAt(dir, name)
- if errors.Is(err, os.ErrNotExist) && r.SeedGuard == nil {
- dir.Close()
- return nil, nil
- }
- if err != nil {
- return fail(err)
- }
- if string(raw) != activationSeedGuardOriginal || r.Phase == "seed-prepared" {
- return fail(errActivation)
- }
- item, err := activationUnitSnapshot(filepath.Join(t.units, "carlos-edge.service.d", name), name)
- if err != nil {
- return fail(err)
- }
- if dry {
- dir.Close()
- return nil, nil
- }
- backup := filepath.Join(t.state, name+".original")
- if err = os.Link(filepath.Join(t.units, "carlos-edge.service.d", name), backup); err != nil && !errors.Is(err, os.ErrExist) {
- return fail(err)
- }
- if !sameActivationFile(filepath.Join(t.units, "carlos-edge.service.d", name), backup) {
- return fail(errActivation)
- }
- // The original inode, metadata and bytes remain outside systemd's search path.
- if err = safefile.WriteAt(int(state.Fd()), name+".mask", []byte(activationSeedGuardReplacement), 0644); err != nil {
- return fail(err)
- }
- if err = state.Sync(); err != nil {
- return fail(err)
- }
- r.SeedGuard = &item
- return dir, nil
-}
-
-func (t activationOffline) seedGuardCheck(r *activationOfflineRecord) error {
- if r.SeedGuard == nil || r.SeedGuard.Name != "90-host-floor.conf" || r.SeedGuard.Masked {
- return errActivation
- }
- g := activationOffline{units: filepath.Join(t.units, "carlos-edge.service.d"), state: t.state}
- if err := g.checkItem(*r.SeedGuard); err != nil {
- return err
- }
- state, err := activationDir(t.state, false)
- if err != nil {
- return err
- }
- defer state.Close()
- raw, err := activationSeedReadAt(state, r.SeedGuard.Name+".mask")
- if err != nil || string(raw) != activationSeedGuardReplacement {
- return errActivation
- }
- return nil
-}
-
-func (t activationOffline) seedGuardReplace(dir, state *os.File, r activationOfflineRecord, restore bool) error {
- if err := t.seedGuardCheck(&r); err != nil {
- return err
- }
- g := activationOffline{units: filepath.Join(t.units, "carlos-edge.service.d"), state: t.state, checkpoint: t.checkpoint}
- return g.replace(dir, state, *r.SeedGuard, restore)
-}
-
-// Transaction-owned originals and replacements deliberately have retained hard
-// links. Keep the ordinary installer reader's single-link restriction intact.
-func activationSeedReadAt(dir *os.File, name string) ([]byte, error) {
- if filepath.Base(name) != name {
- return nil, errActivation
- }
- fd, err := unix.Openat(int(dir.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
- if err != nil {
- return nil, err
- }
- f := os.NewFile(uintptr(fd), name)
- defer f.Close()
- var st unix.Stat_t
- if err := unix.Fstat(fd, &st); err != nil {
- return nil, err
- }
- if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Uid != 0 || st.Mode&0022 != 0 {
- return nil, errActivation
- }
- raw, err := io.ReadAll(io.LimitReader(f, 65537))
- if err != nil {
- return nil, err
- }
- if len(raw) > 65536 {
- return nil, errActivation
- }
- return raw, nil
-}
diff --git a/internal/host/activation_seed_rootrunner_linux_test.go b/internal/host/activation_seed_rootrunner_linux_test.go
deleted file mode 100644
index 7945354d3..000000000
--- a/internal/host/activation_seed_rootrunner_linux_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-//go:build linux && rootrunner
-
-package host
-
-import (
- "errors"
- "os"
- "path/filepath"
- "testing"
-)
-
-func TestRootActivationSeedInterruptions(t *testing.T) {
- for _, point := range []string{"journal-seed-prepared", "stage-90-host-floor.conf", "replace-90-host-floor.conf", "seed-guard-replaced", "seed-guard-restored", "seed-override", "stage-carlos-edge.service", "replace-carlos-edge.service", "seed-reload", "seed-started", "seed-stopped", "seed-remasked", "seed-removed", "journal-held"} {
- t.Run(point, func(t *testing.T) {
- tx := setupActivationSeedCLI(t)
- defer cleanupActivationSeedCLI(t, tx)
- stop := errors.New("seed interruption")
- tx.seedPreflight = activationSeedPreflight
- tx.checkpoint = func(name string) error {
- if name == point {
- return stop
- }
- return nil
- }
- if err := tx.seed(t.Context(), []byte(activationSeedUnit), false); !errors.Is(err, stop) {
- t.Fatal(point, err)
- }
- tx.checkpoint = nil
- if err := tx.seed(t.Context(), []byte(activationSeedUnit), false); err != nil {
- t.Fatal("resume", point, err)
- }
- assertActivationSeedCLIHeld(t, tx)
- activationSeedPrepareRestore(t)
- activationSeedCLI(t, "offline-restore")
- })
- }
-}
-
-func TestRootActivationSeedProductionFailures(t *testing.T) {
- tx := setupActivationSeedCLI(t)
- defer cleanupActivationSeedCLI(t, tx)
- late := filepath.Join(tx.units, "carlos-edge.service.d/99-operator.conf")
- activationProductionWrite(t, late, []byte("[Service]\nExecStart=\nExecStart=/usr/bin/true\n"), 0644)
- if out, err := activationSeedCLICommand(t, "offline-seed", "--dry-run").CombinedOutput(); err == nil {
- t.Fatal("late override accepted", string(out))
- }
- if err := os.Remove(late); err != nil {
- t.Fatal(err)
- }
- activationProductionWrite(t, "/data/activation-payload-fail", nil, 0600)
- if out, err := activationSeedCLICommand(t, "offline-seed").CombinedOutput(); err == nil {
- t.Fatal("failed real seed unit accepted", string(out))
- }
- load, active, err := tx.status(t.Context(), activationEdgeUnit)
- if err != nil || load != "masked" || (active != "inactive" && active != "failed") {
- t.Fatal("failed start escaped", load, active, err)
- }
- if err := os.Remove("/data/activation-payload-fail"); err != nil {
- t.Fatal(err)
- }
- activationSeedCLI(t, "offline-seed")
- assertActivationSeedCLIHeld(t, tx)
- cmd := startActivationSeedCLIBlocked(t)
- if err := cmd.Process.Kill(); err != nil {
- t.Fatal(err)
- }
- cmd.Wait()
- if err := os.Rename("/etc/carlos/host.env", "/etc/carlos/host.env.saved"); err != nil {
- t.Fatal(err)
- }
- if out, err := activationSeedCLICommand(t, "offline-seed").CombinedOutput(); err == nil {
- t.Fatal("preflight succeeded without configuration", string(out))
- }
- load, active, err = tx.status(t.Context(), activationEdgeUnit)
- if err != nil || load != "masked" || (active != "inactive" && active != "failed") {
- t.Fatal("retry preflight did not remask", load, active, err)
- }
- if err := os.Rename("/etc/carlos/host.env.saved", "/etc/carlos/host.env"); err != nil {
- t.Fatal(err)
- }
- if err := os.Remove("/data/activation-payload-wait"); err != nil {
- t.Fatal(err)
- }
- activationSeedCLI(t, "offline-seed")
- assertActivationSeedCLIHeld(t, tx)
- activationSeedPrepareRestore(t)
- activationSeedCLI(t, "offline-restore")
-}
diff --git a/internal/host/activation_settled_linux.go b/internal/host/activation_settled_linux.go
deleted file mode 100644
index 48713a5f7..000000000
--- a/internal/host/activation_settled_linux.go
+++ /dev/null
@@ -1,132 +0,0 @@
-//go:build linux
-
-package host
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "syscall"
-
- "golang.org/x/sys/unix"
-)
-
-func (t activationOffline) updateDir() string {
- if t.updateRoot != "" {
- return t.updateRoot
- }
- return systemRootDir("/opt/carlos")
-}
-
-// Hold the updater's lock through admission, so an already admitted update
-// cannot race the snapshot. Apply rechecks the transaction after taking it.
-func (t activationOffline) settled(ctx context.Context, dry bool) (func(), error) {
- refuse := func(err error) (func(), error) {
- return nil, fmt.Errorf("claim activation: settle the update first: %s", err)
- }
- if err := ctx.Err(); err != nil {
- return refuse(err)
- }
- dir, err := activationDir(t.updateDir(), !dry)
- if errors.Is(err, os.ErrNotExist) && dry {
- if err := t.updateUnitsSettled(ctx); err != nil {
- return nil, err
- }
- return func() {}, t.updateSettled()
- }
- if err != nil {
- return refuse(err)
- }
- defer dir.Close()
- flags := unix.O_RDWR | unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_NONBLOCK
- if !dry {
- flags |= unix.O_CREAT
- }
- fd, err := unix.Openat(int(dir.Fd()), systemLockFile, flags, 0600)
- if errors.Is(err, os.ErrNotExist) && dry {
- if err := t.updateUnitsSettled(ctx); err != nil {
- return nil, err
- }
- return func() {}, t.updateSettled()
- }
- if err != nil {
- return refuse(err)
- }
- lock := os.NewFile(uintptr(fd), filepath.Join(t.updateDir(), systemLockFile))
- st, err := lock.Stat()
- if err != nil || !st.Mode().IsRegular() || st.Mode().Perm()&0022 != 0 || st.Sys().(*syscall.Stat_t).Uid != 0 {
- lock.Close()
- return refuse(errActivation)
- }
- if err := unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB); err != nil {
- lock.Close()
- return refuse(fmt.Errorf("%s: %w", lock.Name(), err))
- }
- if err := t.updateUnitsSettled(ctx); err != nil {
- lock.Close()
- return nil, err
- }
-
- if err := t.updateSettled(); err != nil {
- lock.Close()
- return nil, err
- }
- return func() { lock.Close() }, nil
-}
-
-func (t activationOffline) updateSettled() error {
- for _, path := range []string{
- filepath.Join(t.updateDir(), "journal.json"),
- filepath.Join(t.updateDir(), rearmPendingFile),
- filepath.Join(t.updateDir(), "staging"),
- filepath.Join(filepath.Dir(t.updateDir()), "guard", "update-begun.json"),
- } {
- _, err := os.Lstat(path)
- if errors.Is(err, os.ErrNotExist) {
- continue
- }
- if err == nil {
- err = errActivation
- }
- return fmt.Errorf("claim activation: settle the update first: %s: %w", path, err)
- }
- return nil
-}
-
-func (t activationOffline) updateUnitsSettled(ctx context.Context) error {
- for _, name := range []string{"carlos-system-update.service", "carlos-system-recover.service"} {
- _, active, err := t.status(ctx, name)
- if err != nil || active != "inactive" && active != "failed" {
- return fmt.Errorf("claim activation: settle the update first: %s: active=%s", name, active)
- }
- }
- return nil
-}
-
-// The systemd updater gets CARLOS_OPT_DIR from protected host.env. An
-// operator's shell must not redirect admission to a different lock or journal.
-func activationConfiguredUpdateRoot(config, override string) (string, error) {
- opt := "/opt/carlos"
- raw, err := activationRead(config, false)
- if err != nil && !errors.Is(err, os.ErrNotExist) {
- return "", err
- }
- if err == nil {
- env, err := activationEnvironment(raw)
- if err != nil {
- return "", err
- }
- if env["CARLOS_OPT_DIR"] != "" {
- opt = env["CARLOS_OPT_DIR"]
- }
- }
- if !filepath.IsAbs(opt) || filepath.Clean(opt) != opt {
- return "", errActivation
- }
- if override != "" && override != opt {
- return "", errors.New("claim activation: settle the update first: CARLOS_OPT_DIR differs from protected configuration")
- }
- return systemRootDir(opt), nil
-}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 7623cfb28..b1c7db1b2 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4340,9 +4340,6 @@ func runAdoptWithEnsureSystemUnits(args []string, buildClaims ClaimFactory, ensu
}
func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUnits func(), census func() (rootparticipation.Ledger, bool, error), updateProviders ...release.RootUpdateOpener) error {
- if err := activationAdoptionAllowed("/etc"); err != nil {
- return err
- }
reapStagingWrites(release.OptDir(), dataDir(), "/etc", log.Printf)
if os.Geteuid() == 0 {
if err := repairOffcloudCredentials("/etc/carlos/credentials"); err != nil {
diff --git a/internal/host/systemapply.go b/internal/host/systemapply.go
index 47ed9deeb..9a959dadf 100644
--- a/internal/host/systemapply.go
+++ b/internal/host/systemapply.go
@@ -273,12 +273,6 @@ func (a *systemApplier) file(name string) string { return filepath.Join(a.rootDi
// journal is closed, and re-firing OnFailure= over a finished recovery
// would only start it again with nothing left to do.
func (a *systemApplier) Run(ctx context.Context, recover bool) (retErr error) {
- if err := activationTransactionAllowed(a.etcDir, recover); err != nil {
- if errors.Is(err, errActivationRecoveryHeld) {
- return nil
- }
- return err
- }
defer func() {
if errors.Is(retErr, context.DeadlineExceeded) {
if required, err := a.participationDisk().ParticipationRequired(context.WithoutCancel(ctx)); err == nil && required {
@@ -312,12 +306,6 @@ func (a *systemApplier) Run(ctx context.Context, recover bool) (retErr error) {
a.ops.logf("system apply: another apply is running; leaving it to finish")
return nil
}
- if err := activationTransactionAllowed(a.etcDir, recover); err != nil {
- if errors.Is(err, errActivationRecoveryHeld) {
- return nil
- }
- return err
- }
if a.floorHook != nil {
if err := a.floorHook(a); err != nil {
return err
diff --git a/internal/host/systemunits.go b/internal/host/systemunits.go
index 99d040bd3..2f97f0098 100644
--- a/internal/host/systemunits.go
+++ b/internal/host/systemunits.go
@@ -40,10 +40,6 @@ var systemUnitFiles = []string{"carlos-system-update.path", "carlos-system-updat
// failure to report, and probe asks the two QUESTIONS, where non-zero is
// half the answer (see the block below).
func ensureSystemUnits(etcDir, lockPath string, systemctl, probe func(args ...string) (string, error), logf func(string, ...any)) {
- if err := activationAdoptionAllowed(etcDir); err != nil {
- logf("adopt: system units: %v", err)
- return
- }
if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil {
logf("adopt: system units: %v", err)
return
@@ -64,11 +60,6 @@ func ensureSystemUnits(etcDir, lockPath string, systemctl, probe func(args ...st
return
}
- if err := activationAdoptionAllowed(etcDir); err != nil {
- logf("adopt: system units: %v", err)
- return
- }
-
claimSettings, settingsErr := readClaimUnitSettings(filepath.Join(etcDir, "carlos", "host.env"))
if settingsErr != nil {
logf("adopt: instance-control unit settings: %v", settingsErr)
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 3d7631bba..603bbeda7 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -981,9 +981,9 @@ func checkRootReadDeclarations(scan rootReadScan, inv rootReadInventory) []strin
}
}
for key, removal := range inv.Removed {
- if strings.HasSuffix(removal, ";census-ruling-J4") {
+ if strings.HasSuffix(removal, ";census-ruling-J4") || strings.HasSuffix(removal, ";census-ruling-activation-tools-removal") {
if _, exists := scan.Declarations[key]; exists || inv.Entries[key].Class != "" {
- errs = append(errs, "removed J4 declaration still present "+key)
+ errs = append(errs, "removed declaration still present "+key)
}
continue
}
@@ -1027,3 +1027,26 @@ func TestPlan14J4RemovalRequiresAbsentDeclaration(t *testing.T) {
t.Fatal("unruled removal accepted")
}
}
+
+func TestPlan14ActivationToolsRemovalRequiresAbsentDeclaration(t *testing.T) {
+ key := "root.go:dead"
+ scan := rootReadScan{Declarations: map[string]string{}}
+ inv := rootReadInventory{Entries: map[string]classifiedRootRead{}, Removed: map[string]string{key: "G,J4;census-ruling-activation-tools-removal"}}
+ if errs := checkRootReadDeclarations(scan, inv); len(errs) != 0 {
+ t.Fatal(errs)
+ }
+ scan.Declarations[key] = "digest"
+ if len(checkRootReadDeclarations(scan, inv)) == 0 {
+ t.Fatal("live removed declaration accepted")
+ }
+ delete(scan.Declarations, key)
+ inv.Entries[key] = classifiedRootRead{Class: "authority"}
+ if len(checkRootReadDeclarations(scan, inv)) == 0 {
+ t.Fatal("retained removed row accepted")
+ }
+ delete(inv.Entries, key)
+ inv.Removed[key] = "E2;unruled"
+ if len(checkRootReadDeclarations(scan, inv)) == 0 {
+ t.Fatal("unruled removal accepted")
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-activation-tools.tsv b/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
index f0668007f..e81ebcb64 100644
--- a/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
+++ b/internal/provision/testdata/plan14-root-reads-activation-tools.tsv
@@ -1,15 +1,15 @@
# Activation-tools runs 1–10: proposals only; loaded after all existing overlays.
@coverage . efeeda08e816fc2c7e3aa451b06cce95ab5778af79682290cf98a60d8b9a841a
@coverage internal/claimactivation 625d82dce14bae92f619077ca92a8c9066a9446545dfcc96a6f818e89e20d4c2
-@coverage internal/host aef98870e366deb9774a74d8fb8f85ed2b94e8ea425f63c36b6a3517959665cd
+@coverage internal/host b1b7062482391aa9c97069726ff52cf87c3965fa0fcabae0946daf60fe00bd92
@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
internal/claimactivation/runner.go checkedStore.Get 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 366fec104118e7bce2b71ede13e7c8c72314d99fd32f773967e31018b0f0fbd6
internal/claimactivation/runner.go checkedStore.Put 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@1357 26c6e1f7cb2a057ef8b19deb30bc18fd62fe08f81ff48fd4022bdfc3dd22d6b0
internal/host/activation_command.go RunSystemClaims 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 cc8289fbcb3f34d8d43cd912a6a43ace727ed7289c6c32c5d35c8d00e6194cf3
-internal/host/activation_command_linux.go activationProcessesDrained 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:ReadDir@219,file:Stat@458 104e95618024c8f84acb7f18c39cdc18ac6834d34242a075f4652fdb6ed84e75
-internal/host/activation_command_linux.go runSystemClaims 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:Read@1518 aef08108659e86b3fd122d584b417a82ae0dfdb8cd053e46577c02014d2daeb2
+@removed internal/host/activation_command_linux.go activationProcessesDrained G,J4 census-ruling-activation-tools-removal
+internal/host/activation_command_linux.go runSystemClaims 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:Read@1518 b71131cace86703746375d4525b91e69c35a4702021e8a003545cb2d78eefd0b
internal/host/activation_command_other.go runSystemClaims 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 fcf45d665275c2ac99ff9bfc37302217bb6134b34ff74c3b17f50d1348760151
internal/host/activation_console.go activationConsoleEnvironment 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 9d1144ac45e5849f43872d12ac6389c620ad07f0b2e69575a1353bfaf218b9fd
internal/host/activation_install.go activationEnvironment 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 289ff3c1267e613afd102f83f68592bfd99dafc3d6b7eaba05e630e6b2e7be3e
@@ -22,20 +22,20 @@ 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=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 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=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 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=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume ee2dd65962263ff367b1f72140fd2c5fc5bffd4bfbd6d6d127c94bb68b5e2036
-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
-internal/host/activation_offline_linux.go activationUnitSnapshot 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@135,file:ReadFile@842,file:Readlink@562 c55777e2f221a6e9c6f2ab5a66fb875596e336e0b403b3479d6c7c04d4591188
-internal/host/activation_offline_linux.go sameActivationFile 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@107,file:Lstat@53 72cf415a823352f1fcf5aba8452df074896aacc851da3dfea9a635b37c44669b
+@removed internal/host/activation_offline_linux.go activationOffline.begin G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.checkItem G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.dirs G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.point G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.prepare G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.preview G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.read G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.replace G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.restore G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.save G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.status G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOfflineUnits G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationUnitSnapshot G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go sameActivationFile G,J4 census-ruling-activation-tools-removal
internal/hostfence/activation_credentials.go ActivationCredentialReceipt authority F,J4 rev3=PROPOSED-activation-tools;identity=root fence service and explicit installer uid 0;basis=dedicated root fence credentials retain 0400 compatibility; 0600 needs a matching protected installation receipt; absent explicit installation keeps prior refusal 63ffd51457e632687bc44f776fd5c0335748a899e623ff67a0dea31be4ffa7f2
internal/hostfence/activation_credentials.go ValidateActivationCredentials authority F,J4 rev3=PROPOSED-activation-tools;identity=root fence service and explicit installer uid 0;basis=dedicated root fence credentials retain 0400 compatibility; 0600 needs a matching protected installation receipt; absent explicit installation keeps prior refusal 9a08dd0349bd4bd43ef44d6e621876d43e8c151dcb355ef94c9f134776f0f4d9
internal/hostfence/activation_credentials.go VerifyActivationCredentialReceipt authority F,J4 rev3=PROPOSED-activation-tools;identity=root fence service and explicit installer uid 0;basis=dedicated root fence credentials retain 0400 compatibility; 0600 needs a matching protected installation receipt; absent explicit installation keeps prior refusal bd06fbd520d6808a635f8e3a4a8a5b8a389beb1d1374d56848853b1ad6855d84
@@ -46,40 +46,40 @@ system.go systemVerb authority B,C,D,F,G,J4 rev3=PROPOSED-activation-tools;ident
internal/host/activation_console_report_linux.go activationConsolePID authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=signed roster and identity, protected guard and configuration, live systemd MainPID plus proc boot/namespace/start/cgroup/executable; exact private box report CAS and readback; no automatic call e8ebb8a006f4b8407e377ddb01e39162e0bbc5d88f656bfc07cc4220828281c2
internal/host/activation_console_report_linux.go activationConsoleProcess authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=signed roster and identity, protected guard and configuration, live systemd MainPID plus proc boot/namespace/start/cgroup/executable; exact private box report CAS and readback; no automatic call file:Fstat@5748,file:Open@5443,file:ReadFile@3554,file:ReadFile@3703,file:ReadFile@4156,file:ReadFile@4369,file:ReadFile@4520,file:Stat@4002,file:Stat@5543,file:Stat@5614 29235a700ba8fece0602b97a216a4db9d0d9471e3d59a2da4e913f443fbfa923
internal/host/activation_console_report_linux.go publishActivationConsoleReport authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=signed roster and identity, protected guard and configuration, live systemd MainPID plus proc boot/namespace/start/cgroup/executable; exact private box report CAS and readback; no automatic call file:Read@2593,file:Read@296 f33733cab3dfbeede384906fdfb4649305dbaa27f756964df2abccdab8408cbb
-internal/host/activation_seed_linux.go activationOffline.seed authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 6ac2e84577498a4c8ad1cc91f631aca5c946b531b524453c5172f803fe607125
-internal/host/activation_seed_linux.go activationSeedDropIns authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot file:ReadDir@636 9ba061b030ee71b2782babfc6e9bf42e0ca2a1964b5b46340bec753147637a98
-internal/host/activation_seed_linux.go activationSeedPreflight authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 3d5596c6bb0cb4a9758867c249828915b15c2a5e4822429308e7d71d2b3c3f61
-internal/host/activation_seed_linux.go const activationSeedDropIn authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot e21d702f24a9406e5a889041647e2f24185a6d06b7a8a1d0288c77b5359a85f6
-internal/host/activation_seed_linux.go const activationSeedUnit authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 678e56e4a6a23982f3d6bc8c0f9c465ef8fefb9605dfe36354d3d064ebf22714
+@removed internal/host/activation_seed_linux.go activationOffline.seed G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationSeedDropIns G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationSeedPreflight G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go const activationSeedDropIn G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go const activationSeedUnit G,J4 census-ruling-activation-tools-removal
@coverage scripts 3ce4672ec33aaabdbadf5dd30e28ff6dda9d012bd55936093f57faf9fcabecab
scripts/activation-tools-root-runner.sh external diagnostic G,J4 rev3=PROPOSED-activation-tools;identity=authorized root-runner operator;basis=builds real console and CLI into test-only embedded images; serial designated runner invocation; deletes only owned build artifacts file:external@0 17400986085d3a406062f45c9669f88c833502f7bbde138f300db9d5e8a86a51
-internal/host/activation_seed_linux.go activationOffline.seedGuard authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 61ad6431a4874f1d01dfafa5a246b816becc7e3946b7c72eebbd02ee52c53c03
-internal/host/activation_seed_linux.go activationOffline.seedGuardCheck authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 1deb9a7d2e0c01aebdcc0e190ddcb084577ff6c5155b571cf1ea63066bc40f28
-internal/host/activation_seed_linux.go activationOffline.seedGuardReplace authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 56cdf1c30d97949c89707cfab1524240c3728c21a87a3ee8bb6e7c570afbe750
-internal/host/activation_seed_linux.go activationOffline.seedRemask authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot 3292bedf44f4b105eceb76d38a73c3f73e5700e9e949e822e5314c853fc3ad61
-internal/host/activation_seed_linux.go const activationSeedGuardOriginal authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot de4e081ef3584a30477e20d3d30bcc0d1f3582607be533921c3896d6548440f2
-internal/host/activation_seed_linux.go const activationSeedGuardReplacement authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=exact guard replacement journaled before unmask; retain original bytes metadata and inode outside search path; remask on returned errors and interrupted retry preflight; seed only across crash/reboot d5d0cacb84f15b9d5098e05f41a7981852dc53add1c20b946a1a870ef3dc2736
-internal/host/activation_seed_linux.go activationSeedReadAt authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit operator CLI uid 0;basis=protected directory descriptor; nofollow nonblocking regular-file read; root ownership and bounded bytes; retained hardlinks permitted only in seed configuration inspection file:Fstat@368,file:Openat@148,file:ReadAll@545 0fecdfd3967cc9db0f5a5f2cb81a8eb4d440ffb145ab9ef604c77f654d74e94e
-internal/host/activation_adoption_linux.go activationAdoptionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=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 9f5c63bae1686a4eced50eed59019280bd1c7beb6e1e9f5d1efbd5994e2011d7
-internal/host/activation_adoption_other.go activationAdoptionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. 9a70a93e601ea57fe2ce1780cc6cf878ea3cde656770fe8c400d346fbdbca361
-internal/host/activation_offline_linux.go var activationEdgeUnit authority G,J4 rev3=PROPOSED-activation-tools;identity=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 42c79dbb523cf08c19f23d2e4f1754902524276b86f58f0d3d5bfb6117d3fcc6
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=no census selects legacy proof/history and update effects; proof observation cannot fetch signed recovery manifests on that path; participating adoption retains root release exclusion and independent generation pins whole:Route@11179,whole:Route@18368,whole:Route@18404,whole:Route@8481 2f755c7ed82fcac622a118d1e4ad0b5d746de2b97e0456e125114e12351cd0c8
-internal/host/systemunits.go ensureSystemUnits authority F,J4 rev3=PROPOSED-activation-tools;identity=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:OpenFile@375,file:ReadFile@3129,file:ReadFile@3262,file:ReadFile@4176,file:Stat@3038 eda788ec9bfb20192d503aa2ce07f707871de3bba111830f00bc2be0b5c8daa2
-internal/host/systemapply.go systemApplier.Run authority D,F,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume file:OpenFile@639 6e9f3d6e5af32111448d6e3a48052e411d2e7e38e42475c71fd76cf8d40cf068
-internal/host/activation_adoption_linux.go activationTransactionAllowed authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume file:Lstat@157,file:Stat@377 538b2498363c32a7e54514fa1639ea53acaac1b032ff6adf1bfbd38c7c461c3d
-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=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume ac9fd3519c2d80a41acd20a0a1d8992d0a17c8cd1c57bf2910cba0d03fb2f559
-internal/host/activation_offline_linux.go activationOffline.reconcile authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 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 root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 53ffcf02a01db552c8322cccf7808d009e6236524ae66617f4c333abde524614
-internal/host/activation_offline_linux.go activationOffline.baseline authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 345295b67287a0737a9a2fd557381f23f482e666f930904c2b1855630a1823c1
-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
-internal/host/activation_adoption_linux.go var errActivationRecoveryHeld authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 7fe82a76cf22b8d3f5c8adf31ccb47a2ade429bfb944c51fdc77685f2fe763d1
-internal/host/activation_adoption_other.go var errActivationRecoveryHeld authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 7fe82a76cf22b8d3f5c8adf31ccb47a2ade429bfb944c51fdc77685f2fe763d1
-internal/host/activation_offline_linux.go var activationCanonicalBaseline authority G,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume 2b53e28b44dd1d1910a00059411b1cb2d59c904c483880e95360d84fbf5315b1
+@removed internal/host/activation_seed_linux.go activationOffline.seedGuard G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationOffline.seedGuardCheck G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationOffline.seedGuardReplace G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationOffline.seedRemask G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go const activationSeedGuardOriginal G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go const activationSeedGuardReplacement G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_seed_linux.go activationSeedReadAt G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_adoption_linux.go activationAdoptionAllowed G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_adoption_other.go activationAdoptionAllowed G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go var activationEdgeUnit G,J4 census-ruling-activation-tools-removal
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-activation-tools;identity=root adoption or explicit offline operator;basis=only an explicit protected offline transaction excludes adoption and automatic repair until done; absent transaction preserves existing behavior; complete is durable before edge release; prior authority constraints retained. rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=no census selects legacy proof/history and update effects; proof observation cannot fetch signed recovery manifests on that path; participating adoption retains root release exclusion and independent generation pins whole:Route@11104,whole:Route@18293,whole:Route@18329,whole:Route@8406 a25835004cca39d1c624fc47e59b7bcac47eb175b55c1db583fac920eaa33068
+internal/host/systemunits.go ensureSystemUnits authority F,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:OpenFile@265,file:ReadFile@2908,file:ReadFile@3041,file:ReadFile@3955,file:Stat@2817 984a691e04c28368d7015285d14952add40ce02ce24aab8d3786e80a99af4053
+internal/host/systemapply.go systemApplier.Run authority D,F,J4 rev3=PROPOSED-activation-tools;identity=explicit root operator or root updater;basis=canonical healthy admission; complete excludes writers; settled recovery succeeds without effects for edge dependency; final whole-record verification precedes done on every resume file:OpenFile@483 6a5704be83e35979db7f2811b8ea7fd3d4c2c74c9b532f9f94f1d5482cbeb827
+@removed internal/host/activation_adoption_linux.go activationTransactionAllowed G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_adoption_other.go activationTransactionAllowed G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_settled_linux.go activationOffline.settled G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_settled_linux.go activationOffline.updateSettled G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.closeDifferences G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.reconcile G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_settled_linux.go activationOffline.updateDir G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_settled_linux.go activationOffline.updateUnitsSettled G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationRecordedState G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_settled_linux.go activationConfiguredUpdateRoot G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationSupportedBaseline G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.baseline G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.hold G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go activationOffline.heldDifferences G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_adoption_linux.go var errActivationRecoveryHeld G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_adoption_other.go var errActivationRecoveryHeld G,J4 census-ruling-activation-tools-removal
+@removed internal/host/activation_offline_linux.go var activationCanonicalBaseline G,J4 census-ruling-activation-tools-removal