plan: claim Plan 6b rev 6, review round 5 (flag-off fingerprints, root unit configuration, installer-derived census), full suites run
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+1444
−101
docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md+1444 −101
CI passed — run details
diff --git a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md| index a57e6d163..4de18143a 100644 |
| --- a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md |
| +++ b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md |
| @@ -1,6 +1,6 @@ |
| # Host lifecycle claim, Plan 6b: production box assembly |
| -Implementation plan, 2026-10-07, rev 5. Build on `feat/hostclaim-box-assembly`, cut from `origin/main` at **3407d114f8c8a413214b88295457263bea50dcb7**. Plan 6 landed as `d649d8d4`; this base includes `84c80eba`, region HA plan 1b and the subsequent main changes, including temporary static sites at `f0606ae1`, GeoIP, release-proof changes and the host-claim floor at `cbc0922e` and console/backfill work at `3407d114`. This document supplies the next implementation steps. Authoring it stages nothing, commits nothing and creates no implementation branch. |
| +Implementation plan, 2026-10-07, rev 6. Build on `feat/hostclaim-box-assembly`, cut from `origin/main` at **9cf9f18f53aac403f49ad755089c41c490b7e735**. Plan 6 landed as `d649d8d4`; this base includes `84c80eba`, region HA plan 1b and the subsequent main changes, including temporary static sites at `f0606ae1`, GeoIP, release-proof changes and the host-claim floor at `cbc0922e` and console/backfill work at `3407d114`, moves Phase 1 at `17362eaf` and its fixes through `9cf9f18f`. This document supplies the next implementation steps. Authoring it stages nothing, commits nothing and creates no implementation branch. |
| Rev 2 answers all four round-1 findings: P2-1 moves inbound, config-gate and co-spawn refusals before admission effects (Task 4); P2-2 retains the persisted attempt's boot/controller tuple and routes restart recovery through root's dead-controller proof (Task 7); P2-3 journals recoverable pre-launch state before restore or materialization (Tasks 3, 5 and 7); P2-4 rebases every block onto current main and retains `boxid.Verifier.Enroll` and main's region HA readers (Tasks 1 and 6). Task 2 supplies regression tests; the authoring verification records the executed failures and gates. |
| @@ -10,6 +10,8 @@ Rev 4 answers round 3. P1 removes the agent-side tenant execution fallback and r |
| Rev 5 answers both round-4 findings. The adoption ownership test follows `RunAdopt` through both implementation helpers and still requires ownership-converging registry opens; it is included in the focused gate. The production census classifies `hostguard`, the real guard-record directory. A real reconciler installation uses the production shared-path list, creates the guard record, persists route ownership and inventory, and repeats without changing ownership. An unknown data file still refuses before activation; a durable fence refuses installation without changing the record or creating route ownership or inventory. All blocks are refreshed onto the current main base above, preserving its GeoIP, release-proof, host-claim-floor and console/backfill changes. The instance loop retains main's claim backfill and passes the provisioning configurator alongside its write policy. Wake-retry reset joins main's existing host-resolution dispatcher with its operator and signed-floor checks; declare-lost and readmit remain intact. Authoring verification now runs the full `internal/host` and `internal/hostbox` packages. |
| +Rev 6 answers all three round-5 findings. Both registry fingerprints omit default ownership and tag populated ownership fields, preserving main's hold encoding and its unheld-row golden hashes. Bootstrap and root unit repair render the instance-control service from the installed host.env, including its claim flag and configured data/opt roots; enabled units load that environment, permit IPv4/IPv6 alongside Unix sockets, and grant writes only to the required roots. Changed claim unit configuration restarts an active service; a failed restart restores the old bytes so repair can retry. Flag-off unit bytes remain main's. The empty census derives platform binaries, version stores, embedded units and enablement links from production installer APIs and unit metadata. Its populated fixture runs bootstrap, system unit installation and arming, second-belt repair and release staging. Exact symlink targets, unknown files and host-specific artifacts remain refusals. Every touched package now has a full-suite authoring gate, with failures reproduced on unmodified main reported separately. |
| + |
| > For agentic workers: apply the tasks in order, tests before implementation. Keep `CARLOS_HOST_CLAIM` unset. Do not execute branch creation or deployment commands in the design worktree. The code blocks below are complete new files or exact patches against the pinned main base. Use the extractor below to apply patches. Missing authority refuses the dependent operation before an effect. |
| The work covers review findings 1 through 4 in `.superpowers/sdd/2026-10-05-host-lifecycle-claim-6-box-wiring/review-610.md` from the `box` worktree: startup composition, durable wake retries, reconcile admission and retirement, and root evidence and role launches. It uses claim **rev 6.30**, especially section 1.1 and the ruled P6-D1 through P6-D8 in section 6.6. Plan 6 rev 4 remains implementation history; its landed code is already on main. Do not replay it. |
| @@ -57,7 +59,7 @@ git merge-base --is-ancestor 84c80eba origin/main |
| git switch -c feat/hostclaim-box-assembly origin/main |
| ``` |
| -Expected: a clean implementation checkout and the new assembly branch from the latest fetched main. These blocks were verified at `3407d114f8c8a413214b88295457263bea50dcb7`. If main has moved, compare it with that base and refresh the patches before applying them. Preserve main's `boxid.Verifier.Enroll`, `termfence`, `systemparticipation.go`, `boxrecord`, `edgesclaims` and `boxvolumes`; do not replay region HA plan 1b or reintroduce its superseded readers. Preserve the existing flag-off and hostbox fixes, including main's nested-default layout and startup layout recovery. Keep the main isolated-exec design and its console layout door (`carlos system layout`) intact. These blocks support no enabled launch shape until the root protocol carries and validates the reservation. |
| +Expected: a clean implementation checkout and the new assembly branch from the latest fetched main. These blocks were verified at `9cf9f18f53aac403f49ad755089c41c490b7e735`. If main has moved, compare it with that base and refresh the patches before applying them. Preserve main's `boxid.Verifier.Enroll`, `termfence`, `systemparticipation.go`, `boxrecord`, `edgesclaims` and `boxvolumes`; do not replay region HA plan 1b or reintroduce its superseded readers. Preserve the existing flag-off and hostbox fixes, including main's nested-default layout and startup layout recovery. Keep the main isolated-exec design and its console layout door (`carlos system layout`) intact. These blocks support no enabled launch shape until the root protocol carries and validates the reservation. |
| Read these exact local sources before changing code: |
| @@ -5707,12 +5709,14 @@ func TestClaimWakeRefusesEveryUnisolatedShapeBeforeAdmission(t *testing.T) { |
| package host |
| import ( |
| + "bufio" |
| "bytes" |
| "context" |
| "database/sql" |
| "errors" |
| "os" |
| "path/filepath" |
| + "strings" |
| "testing" |
| "time" |
| @@ -5727,21 +5731,310 @@ import ( |
| ) |
| func TestClaimProductionCensusInstallsThroughReconciler(t *testing.T) { |
| - for _, mode := range []string{"empty", "fenced"} { |
| - t.Run(mode, func(t *testing.T) { |
| - testClaimProductionCensusInstallation(t, mode == "fenced") |
| - }) |
| + for _, flag := range []string{"", "1"} { |
| + for _, mode := range []string{"empty", "fenced"} { |
| + name := "flag-off/" + mode |
| + if flag != "" { |
| + name = "flag-on/" + mode |
| + } |
| + t.Run(name, func(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", flag) |
| + testClaimProductionCensusInstallation(t, mode == "fenced") |
| + }) |
| + } |
| } |
| } |
| -func testClaimProductionCensusInstallation(t *testing.T, fenced bool) { |
| +// claimCensusInstallLinks is the test's own reading of a unit's [Install] |
| +// section: the symlinks `systemctl enable` makes, relative to the unit |
| +// directory. It fails on a key it does not model rather than under-enable. |
| +func claimCensusInstallLinks(t *testing.T, unit string, body []byte) []string { |
| + t.Helper() |
| + var links []string |
| + install := false |
| + scan := bufio.NewScanner(bytes.NewReader(body)) |
| + for scan.Scan() { |
| + line := strings.TrimSpace(scan.Text()) |
| + switch { |
| + case line == "" || line[0] == '#' || line[0] == ';': |
| + continue |
| + case line[0] == '[': |
| + install = line == "[Install]" |
| + continue |
| + case !install: |
| + continue |
| + } |
| + key, value, _ := strings.Cut(line, "=") |
| + for _, v := range strings.Fields(value) { |
| + switch strings.TrimSpace(key) { |
| + case "WantedBy": |
| + links = append(links, v+".wants/"+unit) |
| + case "RequiredBy": |
| + links = append(links, v+".requires/"+unit) |
| + case "Alias": |
| + links = append(links, v) |
| + default: |
| + t.Fatalf("%s: [Install] %s is not modelled by the census fixture", unit, key) |
| + } |
| + } |
| + } |
| + return links |
| +} |
| + |
| +// claimCensusSystemctl stands in for systemctl over a staging root: enable |
| +// writes exactly the links the unit's [Install] metadata names, pointing at |
| +// the installed unit file, as `systemctl enable` does. Nothing reaches the |
| +// live system manager. |
| +func claimCensusSystemctl(t *testing.T, unitsDir string) func(args ...string) (string, error) { |
| + return func(args ...string) (string, error) { |
| + switch args[0] { |
| + case "is-active": |
| + return "inactive\n", nil |
| + case "is-enabled": |
| + return "disabled\n", nil |
| + case "enable": |
| + default: |
| + return "", nil |
| + } |
| + for _, unit := range args[1:] { |
| + target := filepath.Join(unitsDir, unit) |
| + body, err := os.ReadFile(target) |
| + if err != nil { |
| + return "", err |
| + } |
| + for _, rel := range claimCensusInstallLinks(t, unit, body) { |
| + link := filepath.Join(unitsDir, rel) |
| + if old, err := os.Readlink(link); err == nil && old == target { |
| + continue |
| + } |
| + if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil { |
| + return "", err |
| + } |
| + if err := os.Symlink(target, link); err != nil { |
| + return "", err |
| + } |
| + } |
| + } |
| + return "", nil |
| + } |
| +} |
| + |
| +// claimCensusBox provisions a box under a staging root with the production |
| +// installers: bootstrap, the self-update applier's binary rename, unit |
| +// install and arming, ensureSystemUnits' second belt and the release store. |
| +func claimCensusBox(t *testing.T) ClaimRuntimeResources { |
| + t.Helper() |
| ctx := t.Context() |
| - r := ClaimRuntimeResources{DataDir: t.TempDir(), OptDir: t.TempDir(), UnitsDir: t.TempDir()} |
| + root := t.TempDir() |
| + stageEnv := func(stage string) { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Join(stage, "etc", "carlos"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(stage, "etc", "carlos", "host.env"), []byte("CARLOS_HOST_CLAIM="+os.Getenv("CARLOS_HOST_CLAIM")+"\n"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + stageEnv(root) |
| + if err := bootstrap(root, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + etc := filepath.Join(root, "etc") |
| + r := ClaimRuntimeResources{DataDir: filepath.Join(root, "data"), OptDir: filepath.Join(root, "opt", "carlos"), UnitsDir: filepath.Join(etc, "systemd", "system")} |
| + systemctl := claimCensusSystemctl(t, r.UnitsDir) |
| + a := &systemApplier{optDir: r.OptDir, dataDir: r.DataDir, etcDir: etc, ops: sysOps{ |
| + systemctl: systemctl, |
| + run: func(context.Context, time.Duration, string, ...string) (string, error) { return "", nil }, |
| + logf: t.Logf, |
| + }} |
| + // Bootstrap never stages the binary; the applier renames a staged one |
| + // to its production path. |
| + if err := os.MkdirAll(a.stagingDir(), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + staged := filepath.Join(a.stagingDir(), "carlos") |
| + if err := os.WriteFile(staged, []byte("#!/bin/sh\necho carlos\n"), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(staged, systemBinaryPath(r.OptDir)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := copyFileDurable(systemBinaryPath(r.OptDir), systemPrevPath(r.OptDir), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(prevVersionPath(r.OptDir), []byte("v0.1.0\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(prevAllowedPath(r.OptDir), []byte("v0.1.0\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + newDir := filepath.Join(a.stagingDir(), "new") |
| + stageEnv(newDir) |
| + if err := bootstrap(newDir, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writeJSONDurable(filepath.Join(newDir, "plan.json"), currentBootstrapPlan(), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := a.installUnits(ctx, newDir, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + plan, err := stagedPlan(newDir) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := a.arm(plan); err != nil { |
| + t.Fatal(err) |
| + } |
| + ensureSystemUnits(etc, a.lockPath(), systemctl, systemctl, t.Logf) |
| + for _, unit := range plan.Enabled { |
| + body, err := os.ReadFile(filepath.Join(r.UnitsDir, unit)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, rel := range claimCensusInstallLinks(t, unit, body) { |
| + if _, err := os.Readlink(filepath.Join(r.UnitsDir, rel)); err != nil { |
| + t.Fatalf("fixture did not enable %s: %v", unit, err) |
| + } |
| + } |
| + } |
| + for _, app := range []struct{ acct, ver, app string }{{"acct", "v1", "app"}, {"acct", "v2", "app"}, {"", "v1", "ops"}} { |
| + if err := release.StageArtifact(r.OptDir, app.acct, app.ver, app.app, []byte("build "+app.ver)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| for _, name := range []string{"hostguard", ".hostclaims"} { |
| - if err := os.Mkdir(filepath.Join(r.DataDir, name), 0700); err != nil { |
| + if err := os.MkdirAll(filepath.Join(r.DataDir, name), 0o700); err != nil { |
| t.Fatal(err) |
| } |
| } |
| + return r |
| +} |
| + |
| +// claimCensusMutation changes the box and returns how to put it back. |
| +type claimCensusMutation struct { |
| + name string |
| + mutate func(t *testing.T) func() |
| +} |
| + |
| +func claimCensusReplace(t *testing.T, path string, put func() error) func() { |
| + t.Helper() |
| + info, err := os.Lstat(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var target string |
| + var body []byte |
| + if info.Mode()&os.ModeSymlink != 0 { |
| + target, err = os.Readlink(path) |
| + } else { |
| + body, err = os.ReadFile(path) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Remove(path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := put(); err != nil { |
| + t.Fatal(err) |
| + } |
| + return func() { |
| + if err := os.RemoveAll(path); err != nil { |
| + t.Fatal(err) |
| + } |
| + if target != "" { |
| + err = os.Symlink(target, path) |
| + } else { |
| + err = os.WriteFile(path, body, info.Mode().Perm()) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| +} |
| + |
| +func claimCensusCreate(t *testing.T, path string, put func() error) func() { |
| + t.Helper() |
| + // Restore removes the outermost directory this created, so a host-named |
| + // drop-in directory does not outlive its case. |
| + created := path |
| + for dir := filepath.Dir(path); ; dir = filepath.Dir(dir) { |
| + if _, err := os.Lstat(dir); err == nil { |
| + break |
| + } |
| + created = dir |
| + } |
| + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := put(); err != nil { |
| + t.Fatal(err) |
| + } |
| + return func() { |
| + if err := os.RemoveAll(created); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| +} |
| + |
| +func claimCensusMutations(r ClaimRuntimeResources, host string) []claimCensusMutation { |
| + unit := func(name string) string { return filepath.Join(r.UnitsDir, name) } |
| + edgeLink := unit("multi-user.target.wants/carlos-edge.service") |
| + socketLink := unit("sockets.target.wants/carlos-instance-control.socket") |
| + relink := func(link, target string) func(t *testing.T) func() { |
| + return func(t *testing.T) func() { |
| + return claimCensusReplace(t, link, func() error { return os.Symlink(target, link) }) |
| + } |
| + } |
| + newLink := func(link, target string) func(t *testing.T) func() { |
| + return func(t *testing.T) func() { |
| + return claimCensusCreate(t, link, func() error { return os.Symlink(target, link) }) |
| + } |
| + } |
| + newFile := func(path, body string) func(t *testing.T) func() { |
| + return func(t *testing.T) func() { |
| + return claimCensusCreate(t, path, func() error { return os.WriteFile(path, []byte(body), 0o644) }) |
| + } |
| + } |
| + live := func(h string) func(t *testing.T) func() { |
| + return func(t *testing.T) func() { |
| + link := release.LiveLink(r.OptDir, h) |
| + return claimCensusCreate(t, link, func() error { return release.PointLive(r.OptDir, h, "acct", "v1", "app") }) |
| + } |
| + } |
| + return []claimCensusMutation{ |
| + {"unknown data file", newFile(filepath.Join(r.DataDir, "unclassified.db"), "old")}, |
| + {"unknown opt file", newFile(filepath.Join(r.OptDir, "unexplained"), "old")}, |
| + {"unknown vendor link", newLink(unit("multi-user.target.wants/ssh.service"), "/usr/lib/systemd/system/ssh.service")}, |
| + {"dangling unknown link", newLink(unit("multi-user.target.wants/unknown.service"), "/missing/unit.service")}, |
| + {"unknown unit", newFile(unit("backup.service"), "[Service]\nExecStart=/bin/true\n")}, |
| + {"unknown platform unit", newFile(unit("carlos-extra.service"), "[Service]\nExecStart=/bin/true\n")}, |
| + {"selected host live link", live(host)}, |
| + {"another host live link", live("other.example")}, |
| + {"enable link to the host live link", relink(edgeLink, release.LiveLink(r.OptDir, host))}, |
| + {"enable link to another platform unit", relink(edgeLink, unit("carlos-app@.service"))}, |
| + {"enable link to a host unit instance", relink(socketLink, unit("carlos-app@"+host+".socket"))}, |
| + {"relative enable link", relink(edgeLink, "../carlos-edge.service")}, |
| + {"enable link under the wrong target", newLink(unit("timers.target.wants/carlos-edge.service"), unit("carlos-edge.service"))}, |
| + {"masked platform unit", relink(unit("carlos-edge.service"), "/dev/null")}, |
| + {"platform unit symlinked to a release", relink(unit("carlos-edge.service"), release.VersionPath(r.OptDir, "acct", "v1", "app"))}, |
| + {"enabled host unit instance", newLink(unit("multi-user.target.wants/carlos-app@other.example.service"), unit("carlos-app@.service"))}, |
| + {"host unit drop-in", newFile(unit("carlos-app@"+host+".service.d/10-caps.conf"), "[Service]\n")}, |
| + {"platform unit drop-in", newFile(unit("carlos-edge.service.d/override.conf"), "[Service]\n")}, |
| + {"binary symlinked to a release", relink(systemBinaryPath(r.OptDir), release.VersionPath(r.OptDir, "acct", "v1", "app"))}, |
| + {"symlink in the release store", newLink(release.VersionPath(r.OptDir, "acct", "v1", "shell"), "/bin/sh")}, |
| + {"host-named release", newFile(release.VersionPath(r.OptDir, "acct", "v1", host), "build")}, |
| + {"foreign link into a host live link", newLink(unit("multi-user.target.wants/backup2.service"), release.LiveLink(r.OptDir, "other.example"))}, |
| + {"foreign link into the data root", newLink(unit("multi-user.target.wants/backup3.service"), filepath.Join(r.DataDir, "unit.service"))}, |
| + {"foreign unit naming the platform", newFile(unit("evil.service"), "[Service]\nExecStart="+release.LiveLink(r.OptDir, "other.example")+"\n")}, |
| + {"foreign unit naming a managed root", newFile(unit("export.service"), "[Service]\nExecStart=/usr/bin/cp "+filepath.Join(r.DataDir, "registry.db")+" /srv\n")}, |
| + } |
| +} |
| + |
| +func testClaimProductionCensusInstallation(t *testing.T, fenced bool) { |
| + ctx := t.Context() |
| + r := claimCensusBox(t) |
| reg, err := registry.Open(filepath.Join(r.DataDir, "registry.db")) |
| if err != nil { |
| t.Fatal(err) |
| @@ -5825,26 +6118,24 @@ func testClaimProductionCensusInstallation(t *testing.T, fenced bool) { |
| } |
| return |
| } |
| - // Unknown files still refuse before activation; shared metadata does not |
| - // make arbitrary data files eligible for a new ownership stamp. |
| - unknown := filepath.Join(r.DataDir, "unclassified.db") |
| - if err := os.WriteFile(unknown, []byte("old"), 0600); err != nil { |
| - t.Fatal(err) |
| - } |
| - if err := adapter.Install(ctx, selected, func(registry.ClaimOwnership) error { t.Fatal("unknown file admitted"); return nil }); !errors.Is(err, hostbox.ErrTypedInventory) { |
| - t.Fatalf("unknown file refusal: %v", err) |
| - } |
| - if _, err := os.Stat(guardPath); !errors.Is(err, os.ErrNotExist) { |
| - t.Fatalf("refused installation activated guard: %v", err) |
| - } |
| - if row, err := reg.Get(record.Host); err != nil || row != nil { |
| - t.Fatalf("refused installation wrote a row: %+v %v", row, err) |
| - } |
| - if _, err := inventory.ReadInstallation(ctx, record.Host, obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp); !errors.Is(err, sql.ErrNoRows) { |
| - t.Fatalf("refused installation wrote inventory: %v", err) |
| - } |
| - if err := os.Remove(unknown); err != nil { |
| - t.Fatal(err) |
| + // Unknown files and substituted links still refuse before activation; |
| + // the installers' shared artifacts do not make any of them eligible for |
| + // a new ownership stamp. |
| + for _, m := range claimCensusMutations(r, record.Host) { |
| + restore := m.mutate(t) |
| + if err := adapter.Install(ctx, selected, func(registry.ClaimOwnership) error { t.Fatalf("%s: admitted", m.name); return nil }); !errors.Is(err, hostbox.ErrTypedInventory) { |
| + t.Fatalf("%s: refusal: %v", m.name, err) |
| + } |
| + if _, err := os.Stat(guardPath); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("%s: refused installation activated guard: %v", m.name, err) |
| + } |
| + if row, err := reg.Get(record.Host); err != nil || row != nil { |
| + t.Fatalf("%s: refused installation wrote a row: %+v %v", m.name, row, err) |
| + } |
| + if _, err := inventory.ReadInstallation(ctx, record.Host, obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp); !errors.Is(err, sql.ErrNoRows) { |
| + t.Fatalf("%s: refused installation wrote inventory: %v", m.name, err) |
| + } |
| + restore() |
| } |
| writes := 0 |
| install := func(owner registry.ClaimOwnership) error { |
| @@ -5860,22 +6151,528 @@ func testClaimProductionCensusInstallation(t *testing.T, fenced bool) { |
| return reg.Upsert(route) |
| } |
| if err := adapter.Install(ctx, selected, install); err != nil { |
| - t.Fatalf("first installation with production shared paths: %v", err) |
| + t.Fatalf("first installation on a provisioned box: %v", err) |
| } |
| row, err := reg.Get(record.Host) |
| if err != nil || row == nil || !row.ClaimOwnership.Valid() || writes != 1 { |
| t.Fatalf("installed row=%+v writes=%d err=%v", row, writes, err) |
| } |
| - in, err := inventory.ReadInstallation(ctx, record.Host, obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp) |
| - if err != nil || !in.Complete || len(in.Entries) != 1 || in.Entries[0].Kind != "route" || in.Entries[0].Materialization != "materialized" || in.Entries[0].Ownership != row.ClaimOwner { |
| - t.Fatalf("installation inventory=%+v err=%v", in, err) |
| + in, err := inventory.ReadInstallation(ctx, record.Host, obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp) |
| + if err != nil || !in.Complete || len(in.Entries) != 1 || in.Entries[0].Kind != "route" || in.Entries[0].Materialization != "materialized" || in.Entries[0].Ownership != row.ClaimOwner { |
| + t.Fatalf("installation inventory=%+v err=%v", in, err) |
| + } |
| + if err := adapter.Install(ctx, selected, install); err != nil { |
| + t.Fatalf("repeat installation: %v", err) |
| + } |
| + again, err := reg.Get(record.Host) |
| + if err != nil || again == nil || again.ClaimOwnership != row.ClaimOwnership || writes != 2 { |
| + t.Fatalf("repeat changed ownership: row=%+v writes=%d err=%v", again, writes, err) |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/host/claimunit_test.go` |
| + |
| +```go |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestMain(m *testing.M) { |
| + root, err := os.MkdirTemp("", "host-bootstrap-env-") |
| + if err != nil { |
| + panic(err) |
| + } |
| + claimBootstrapHostEnv = filepath.Join(root, "host.env") |
| + code := m.Run() |
| + if err := os.RemoveAll(root); err != nil { |
| + panic(err) |
| + } |
| + os.Exit(code) |
| +} |
| + |
| +func TestClaimControlUnitInstallers(t *testing.T) { |
| + legacy, err := unitsFS.ReadFile("units/" + claimControlUnit) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, flag := range []string{"", " ", "1", "0"} { |
| + t.Run("flag="+flag, func(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", flag) |
| + enabled := strings.TrimSpace(flag) != "" |
| + check := func(path string) { |
| + t.Helper() |
| + got, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if !enabled { |
| + if !bytes.Equal(got, legacy) { |
| + t.Fatal("flag-off unit differs from main") |
| + } |
| + return |
| + } |
| + props := map[string][]string{} |
| + for _, line := range strings.Split(string(got), "\n") { |
| + k, v, ok := strings.Cut(line, "=") |
| + if ok { |
| + props[k] = append(props[k], v) |
| + } |
| + } |
| + for key, want := range map[string][]string{ |
| + "EnvironmentFile": {"-/etc/carlos/host.env"}, |
| + "ReadWritePaths": {"/run/carlos-control /run/carlos /data /opt/carlos /etc/systemd/system /sys/fs/cgroup"}, |
| + "RestrictAddressFamilies": {"AF_UNIX AF_INET AF_INET6"}, |
| + "ProtectSystem": {"strict"}, |
| + "ProtectHome": {"yes"}, |
| + } { |
| + if !reflect.DeepEqual(props[key], want) { |
| + t.Errorf("%s = %q, want %q", key, props[key], want) |
| + } |
| + } |
| + } |
| + writeEnv := func(etc string) { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("CARLOS_HOST_CLAIM=\""+flag+"\"\n"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + stage := t.TempDir() |
| + writeEnv(filepath.Join(stage, "etc")) |
| + if enabled { |
| + t.Setenv("CARLOS_HOST_CLAIM", "") |
| + } else { |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + } |
| + if err := bootstrap(stage, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + check(filepath.Join(stage, "etc/systemd/system", claimControlUnit)) |
| + f := newApplyFixture(t, "stable", "v1.0.0") |
| + if err := f.a.installUnits(t.Context(), stage, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + check(filepath.Join(f.a.etcDir, "systemd/system", claimControlUnit)) |
| + if enabled && !strings.Contains(strings.Join(f.calls, "\n"), "systemctl try-restart "+claimControlUnit) { |
| + t.Fatal("apply did not restart the control service") |
| + } |
| + |
| + // Repair a running, armed box whose root unit still has the other mode. |
| + etc := t.TempDir() |
| + writeEnv(etc) |
| + units := filepath.Join(etc, "systemd/system") |
| + if err := os.MkdirAll(units, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, name := range systemUnitFiles { |
| + b, e := unitsFS.ReadFile("units/" + name) |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + if name == claimControlUnit && !enabled { |
| + b = append(append([]byte{}, b...), []byte("\n[Service]\nEnvironmentFile=-/etc/carlos/host.env\n")...) |
| + } |
| + if e = os.WriteFile(filepath.Join(units, name), b, 0644); e != nil { |
| + t.Fatal(e) |
| + } |
| + } |
| + var calls []string |
| + ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } |
| + probe := func(args ...string) (string, error) { |
| + if args[0] == "is-active" { |
| + return "active\n", nil |
| + } |
| + return "enabled\n", nil |
| + } |
| + lock := filepath.Join(t.TempDir(), "lock") |
| + ensureSystemUnits(etc, lock, ctl, probe, t.Logf) |
| + check(filepath.Join(units, claimControlUnit)) |
| + if !strings.Contains(strings.Join(calls, "\n"), "try-restart "+claimControlUnit) { |
| + t.Fatal("stale armed unit was not repaired") |
| + } |
| + calls = nil |
| + ensureSystemUnits(etc, lock, ctl, probe, t.Logf) |
| + if len(calls) != 0 { |
| + t.Fatalf("unchanged unit was reinstalled: %v", calls) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestClaimControlUnitSettings(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + t.Setenv("CARLOS_DATA_DIR", "/wrong") |
| + t.Setenv("CARLOS_OPT_DIR", "/wrong") |
| + path := filepath.Join(t.TempDir(), "host.env") |
| + missing, err := readClaimUnitSettings(path) |
| + if err != nil || missing.enabled || missing.data != "/data" || missing.opt != "/opt/carlos" { |
| + t.Fatalf("missing env: %+v %v", missing, err) |
| + } |
| + |
| + for _, tc := range []struct { |
| + name, body string |
| + fail bool |
| + enabled bool |
| + data, opt string |
| + }{ |
| + {"absent flag", "OTHER=value\n", false, false, "/data", "/opt/carlos"}, |
| + {"quoted", "CARLOS_HOST_CLAIM='0'\nCARLOS_DATA_DIR=\"/srv/my data\"\nCARLOS_OPT_DIR=/srv/opt%box\n", false, true, "/srv/my data", "/srv/opt%box"}, |
| + {"last assignment", "CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM=\" \"\n", false, false, "/data", "/opt/carlos"}, |
| + {"multiline", "OTHER='line\nCARLOS_HOST_CLAIM=1\n'\n", true, false, "", ""}, |
| + {"root", "CARLOS_HOST_CLAIM=1\nCARLOS_DATA_DIR=/\n", true, false, "", ""}, |
| + {"relative", "CARLOS_HOST_CLAIM=1\nCARLOS_OPT_DIR=relative\n", true, false, "", ""}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + if err := os.WriteFile(path, []byte(tc.body), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + cfg, err := readClaimUnitSettings(path) |
| + if (err != nil) != tc.fail { |
| + t.Fatalf("settings: %+v %v", cfg, err) |
| + } |
| + if tc.fail { |
| + return |
| + } |
| + if cfg.enabled != tc.enabled || cfg.data != tc.data || cfg.opt != tc.opt { |
| + t.Fatalf("settings: %+v", cfg) |
| + } |
| + if tc.name == "quoted" { |
| + raw, _ := unitsFS.ReadFile("units/" + claimControlUnit) |
| + got := string(claimUnitContent(claimControlUnit, raw, cfg)) |
| + if !strings.Contains(got, `"/srv/my data" "/srv/opt%%box"`) { |
| + t.Fatal(got) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestClaimControlRestartFailureRemainsRepairable(t *testing.T) { |
| + path := filepath.Join(t.TempDir(), claimControlUnit) |
| + before, _ := unitsFS.ReadFile("units/" + claimControlUnit) |
| + after := claimUnitContent(claimControlUnit, before, claimUnitSettings{enabled: true, data: "/data", opt: "/opt/carlos"}) |
| + if err := os.WriteFile(path, after, 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + sentinel := errors.New("restart failed") |
| + calls := 0 |
| + err := restartClaimControl(path, before, after, func(args ...string) (string, error) { |
| + calls++ |
| + if args[0] == "try-restart" { |
| + return "", sentinel |
| + } |
| + return "", nil |
| + }) |
| + if !errors.Is(err, sentinel) || calls != 2 { |
| + t.Fatalf("restart: %v calls=%d", err, calls) |
| + } |
| + got, err := os.ReadFile(path) |
| + if err != nil || !bytes.Equal(got, before) { |
| + t.Fatalf("not restored: %v", err) |
| + } |
| + if !claimControlChanged(got, after) { |
| + t.Fatal("next repair would not retry") |
| + } |
| +} |
| + |
| +func TestClaimBadUnitSettingsStillRepairSystemUnits(t *testing.T) { |
| + etc := t.TempDir() |
| + if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("OTHER='multiline\n"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + units := filepath.Join(etc, "systemd", "system") |
| + if err := os.MkdirAll(units, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + old := []byte("old control unit") |
| + if err := os.WriteFile(filepath.Join(units, claimControlUnit), old, 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + var calls []string |
| + ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } |
| + probe := func(args ...string) (string, error) { return "inactive\n", nil } |
| + ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), ctl, probe, t.Logf) |
| + got, err := os.ReadFile(filepath.Join(units, claimControlUnit)) |
| + if err != nil || !bytes.Equal(got, old) { |
| + t.Fatal("control unit changed on unknown settings") |
| + } |
| + for _, name := range systemUnitFiles { |
| + if name != claimControlUnit { |
| + if _, err := os.Stat(filepath.Join(units, name)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + if !strings.Contains(strings.Join(calls, "\n"), "daemon-reload") { |
| + t.Fatal("system units were not repaired") |
| + } |
| + if strings.Contains(strings.Join(calls, "\n"), "try-restart") { |
| + t.Fatal("unknown settings restarted control service") |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/hostbox/empty_census_classes_test.go` |
| + |
| +```go |
| +//go:build unix |
| + |
| +package hostbox |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +func TestEmptyCensusClassifiesSharedEntriesExactly(t *testing.T) { |
| + ctx := context.Background() |
| + base := t.TempDir() |
| + db, err := OpenStopInventoryDB(filepath.Join(base, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer db.Close() |
| + reg, err := registry.Open(filepath.Join(base, "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer reg.Close() |
| + launches, err := OpenLaunchJournal(filepath.Join(base, "launch.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer launches.Close() |
| + roots := map[string]string{} |
| + for _, name := range []string{"data", "opt", "units"} { |
| + roots[name] = filepath.Join(base, name) |
| + if err = os.Mkdir(roots[name], 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + at := func(root, rel string) string { return filepath.Join(roots[root], rel) } |
| + write := func(path string) { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte("x"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + link := func(target, path string) { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(target, path); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + write(at("data", "opaque/anything")) |
| + write(at("opt", "bin")) |
| + write(at("opt", "store/v1/app")) |
| + write(at("units", "a.service")) |
| + link(at("units", "a.service"), at("units", "multi-user.target.wants/a.service")) |
| + shared := map[string][]string{ |
| + "data": {"opaque"}, |
| + "opt": {CensusFile("bin"), CensusTree("store")}, |
| + "units": {CensusFile("a.service"), CensusLink("multi-user.target.wants/a.service", at("units", "a.service"))}, |
| + } |
| + c := EmptyInstallationCensus{Inventory: db, Registry: reg, Launches: launches, Box: "box", Enrollment: "enrollment", DirectoryRevision: "revision", Roots: roots, SharedPaths: shared} |
| + obs := hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 1}}} |
| + selected := RecordSelection{Host: obs.Host, Incarnation: "inc"} |
| + if err = c.CheckLocal(ctx, obs, selected); err != nil { |
| + t.Fatalf("classified box: %v", err) |
| + } |
| + refuse := func(name string, mutate func() func()) { |
| + t.Helper() |
| + restore := mutate() |
| + if err := c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| + t.Fatalf("%s: %v", name, err) |
| + } |
| + restore() |
| + if err := c.CheckLocal(ctx, obs, selected); err != nil { |
| + t.Fatalf("%s: not restored: %v", name, err) |
| + } |
| + } |
| + remove := func(path string) func() { |
| + return func() { |
| + if err := os.RemoveAll(path); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + created := func(path string, put func(string)) func() func() { |
| + return func() func() { put(path); return remove(path) } |
| + } |
| + swapped := func(path, target string) func() func() { |
| + return func() func() { |
| + old, err := os.Readlink(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + remove(path)() |
| + link(target, path) |
| + return func() { remove(path)(); link(old, path) } |
| + } |
| + } |
| + refuse("symlink in a shared tree", created(at("opt", "store/v1/sh"), func(p string) { link("/bin/sh", p) })) |
| + refuse("host name in a shared tree", created(at("opt", "store/v1/shop.example"), write)) |
| + refuse("unclassified file", created(at("opt", "other"), write)) |
| + refuse("substituted link target", swapped(at("units", "multi-user.target.wants/a.service"), at("units", "b.service"))) |
| + refuse("relative link target", swapped(at("units", "multi-user.target.wants/a.service"), "../a.service")) |
| + refuse("foreign link into a managed root", created(at("units", "multi-user.target.wants/b.service"), func(p string) { link(at("opt", "bin"), p) })) |
| + refuse("foreign link below a unit-root directory", created(at("units", "multi-user.target.wants/c.service"), func(p string) { link(at("units", "multi-user.target.wants/ssh.service"), p) })) |
| + refuse("foreign link naming the platform", created(at("units", "d.service"), func(p string) { link("/srv/carlos/d.service", p) })) |
| + refuse("foreign file naming the host", created(at("units", "e.service"), func(p string) { |
| + if err := os.WriteFile(p, []byte("ExecStart=/srv/shop.example\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + })) |
| + refuse("host-named foreign unit", created(at("units", "x@shop.example.service"), write)) |
| + refuse("file where a link is classified", func() func() { |
| + path := at("units", "multi-user.target.wants/a.service") |
| + remove(path)() |
| + write(path) |
| + return func() { remove(path)(); link(at("units", "a.service"), path) } |
| + }) |
| + if err = os.Remove(at("opt", "bin")); err != nil { |
| + t.Fatal(err) |
| + } |
| + link(at("opt", "store/v1/app"), at("opt", "bin")) |
| + if err = c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| + t.Fatalf("symlink where a file is classified: %v", err) |
| + } |
| + remove(at("opt", "bin"))() |
| + for name, bad := range map[string][]string{"relative link target": {CensusLink("x", "y")}, "escaping entry": {CensusFile("../x")}, "host entry": {CensusFile("shop.example")}, "conflicting entries": {CensusFile("x"), CensusTree("x")}} { |
| + c.SharedPaths = map[string][]string{"data": shared["data"], "opt": append(append([]string{}, shared["opt"]...), bad...), "units": shared["units"]} |
| + if err = c.CheckLocal(ctx, obs, selected); !errors.Is(err, ErrTypedInventory) { |
| + t.Fatalf("%s accepted: %v", name, err) |
| + } |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/registry/claimfingerprint_test.go` |
| + |
| +```go |
| +package registry |
| + |
| +import "testing" |
| + |
| +func claimFingerprints(t *testing.T, r *Registry) (string, string) { |
| + t.Helper() |
| + f, err := r.Fingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + s, err := r.SnapshotFingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return f, s |
| +} |
| + |
| +// Each ownership column moves both fingerprints on its own, partial |
| +// ownership included, and clearing it returns the exact default digests. |
| +func TestFingerprintsSeeEachClaimOwnershipField(t *testing.T) { |
| + reg := openTestRegistry(t) |
| + if err := reg.Upsert(Route{Host: "c", Kind: "instance", Socket: "/s", App: "a", CreatedAt: 1700000000}); err != nil { |
| + t.Fatal(err) |
| + } |
| + defF, defS := claimFingerprints(t, reg) |
| + |
| + set := func(assign string, args ...any) { |
| + t.Helper() |
| + if _, err := reg.sql.Exec(`UPDATE routes SET `+assign+` WHERE host = 'c'`, args...); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + const clear = `claim_incarnation = '', claim_stamp = 0, claim_owner = '', claim_row_id = ''` |
| + |
| + cases := []struct { |
| + name string |
| + assign string |
| + args []any |
| + }{ |
| + {"incarnation", `claim_incarnation = ?`, []any{"i1"}}, |
| + {"zero-incarnation", `claim_incarnation = ?`, []any{"0"}}, |
| + {"zero-owner", `claim_owner = ?`, []any{"0"}}, |
| + {"zero-row_id", `claim_row_id = ?`, []any{"0"}}, |
| + {"stamp", `claim_stamp = ?`, []any{7}}, |
| + {"owner", `claim_owner = ?`, []any{"o1"}}, |
| + {"row_id", `claim_row_id = ?`, []any{"r1"}}, |
| + {"incarnation+owner", `claim_incarnation = ?, claim_owner = ?`, []any{"i1", "o1"}}, |
| + {"stamp+row_id", `claim_stamp = ?, claim_row_id = ?`, []any{7, "r1"}}, |
| + {"full", `claim_incarnation = ?, claim_stamp = ?, claim_owner = ?, claim_row_id = ?`, []any{"i1", 7, "o1", "r1"}}, |
| + } |
| + seenF := map[string]string{} |
| + seenS := map[string]string{} |
| + for _, c := range cases { |
| + set(c.assign, c.args...) |
| + f, s := claimFingerprints(t, reg) |
| + if f == defF { |
| + t.Errorf("%s: Fingerprint did not change", c.name) |
| + } |
| + if s == defS { |
| + t.Errorf("%s: SnapshotFingerprint did not change", c.name) |
| + } |
| + if prev, dup := seenF[f]; dup { |
| + t.Errorf("%s: Fingerprint collides with %s", c.name, prev) |
| + } |
| + if prev, dup := seenS[s]; dup { |
| + t.Errorf("%s: SnapshotFingerprint collides with %s", c.name, prev) |
| + } |
| + seenF[f], seenS[s] = c.name, c.name |
| + |
| + set(clear) |
| + if f, s := claimFingerprints(t, reg); f != defF || s != defS { |
| + t.Errorf("%s: cleared ownership did not restore default digests", c.name) |
| + } |
| + } |
| +} |
| + |
| +// The same value in different ownership fields must not hash alike. |
| +func TestFingerprintsDistinguishClaimFieldsHoldingSameValue(t *testing.T) { |
| + reg := openTestRegistry(t) |
| + if err := reg.Upsert(Route{Host: "c", Kind: "instance", Socket: "/s", CreatedAt: 1700000000}); err != nil { |
| + t.Fatal(err) |
| + } |
| + digests := func(assign string) (string, string) { |
| + t.Helper() |
| + if _, err := reg.sql.Exec(`UPDATE routes SET claim_incarnation = '', claim_owner = '', claim_row_id = '', ` + assign + ` WHERE host = 'c'`); err != nil { |
| + t.Fatal(err) |
| + } |
| + return claimFingerprints(t, reg) |
| } |
| - if err := adapter.Install(ctx, selected, install); err != nil { |
| - t.Fatalf("repeat installation: %v", err) |
| + fa, sa := digests(`claim_owner = 'x'`) |
| + fb, sb := digests(`claim_row_id = 'x'`) |
| + fc, sc := digests(`claim_incarnation = 'x'`) |
| + if fa == fb || fa == fc || fb == fc { |
| + t.Error("Fingerprint aliases ownership fields") |
| } |
| - again, err := reg.Get(record.Host) |
| - if err != nil || again == nil || again.ClaimOwnership != row.ClaimOwnership || writes != 2 { |
| - t.Fatalf("repeat changed ownership: row=%+v writes=%d err=%v", again, writes, err) |
| + if sa == sb || sa == sc || sb == sc { |
| + t.Error("SnapshotFingerprint aliases ownership fields") |
| } |
| } |
| ``` |
| @@ -7586,9 +8383,11 @@ type EmptyInstallationCensus struct { |
| Launches *LaunchJournal |
| Box, Enrollment, DirectoryRevision string |
| Roots map[string]string |
| - // The factory owns this closed list of shared metadata and archives. |
| - // Hostguard checks its own records and fences; excluding that directory |
| - // here never assigns its contents to an installation artifact inventory. |
| + // The factory owns this closed list of shared metadata and archives, |
| + // keyed by root, in the entry forms CensusFile, CensusTree, CensusLink |
| + // build. Hostguard checks its own records and |
| + // fences; excluding that directory here never assigns its contents to an |
| + // installation artifact inventory. |
| SharedPaths map[string][]string |
| } |
| @@ -7658,12 +8457,9 @@ func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.O |
| if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { |
| return ErrTypedInventory |
| } |
| - shared := map[string]bool{} |
| - for _, rel := range c.SharedPaths[name] { |
| - if !managedName(rel) || strings.Contains(rel, selected.Host) { |
| - return ErrTypedInventory |
| - } |
| - shared[rel] = true |
| + rules, err := censusRules(c.SharedPaths[name], selected.Host) |
| + if err != nil { |
| + return err |
| } |
| err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error { |
| if walkErr != nil { |
| @@ -7679,24 +8475,28 @@ func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.O |
| if rel == "." { |
| return nil |
| } |
| - if strings.Contains(rel, selected.Host) || strings.Contains(rel, strings.ReplaceAll(selected.Host, "-", `\x2d`)) { |
| + rel = filepath.ToSlash(rel) |
| + if censusNamesHost(rel, selected.Host) { |
| return ErrTypedInventory |
| } |
| - if shared[rel] { |
| - if d.Type()&os.ModeSymlink != 0 { |
| - return ErrTypedInventory |
| - } |
| - if d.IsDir() { |
| - return filepath.SkipDir |
| - } |
| - return nil |
| + if rule, ok := rules[rel]; ok { |
| + return rule.check(path, d) |
| } |
| - // Conservatively refuse unclassified files from any installation. This |
| - // covers flat, nested and unknown legacy layouts without guessing owners. |
| - if !d.IsDir() { |
| + if censusInTree(rules, rel) { |
| + // Shared release content: directories and regular files only. |
| + if d.IsDir() || d.Type().IsRegular() { |
| + return nil |
| + } |
| return ErrTypedInventory |
| } |
| - return nil |
| + // A directory carries no ownership by itself; every entry below |
| + // it is censused. Conservatively refuse unclassified files from |
| + // any installation. This covers flat, nested and unknown legacy |
| + // layouts without guessing owners. |
| + if d.IsDir() { |
| + return nil |
| + } |
| + return ErrTypedInventory |
| }) |
| if err != nil { |
| return err |
| @@ -7704,6 +8504,113 @@ func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.O |
| } |
| return nil |
| } |
| + |
| +// SharedPaths entries. An untagged entry is an opaque shared file or |
| +// directory whose contents this census does not read (hostguard and the |
| +// claim journals check their own records). The tagged forms are the precise |
| +// classes. None of them assigns anything to an installation. |
| +const ( |
| + censusFileTag = "file:" |
| + censusTreeTag = "tree:" |
| + censusLinkTag = "link:" |
| +) |
| + |
| +// CensusFile classifies rel as one shared regular file. |
| +func CensusFile(rel string) string { return censusFileTag + rel } |
| + |
| +// CensusTree classifies rel as a shared directory whose whole subtree may |
| +// hold only directories and regular files, still checked for the host name. |
| +func CensusTree(rel string) string { return censusTreeTag + rel } |
| + |
| +// CensusLink classifies rel as a symlink whose raw target is exactly target. |
| +func CensusLink(rel, target string) string { return censusLinkTag + rel + "\x00" + target } |
| + |
| +type censusRule struct { |
| + kind string |
| + target string |
| +} |
| + |
| +func censusRules(entries []string, host string) (map[string]censusRule, error) { |
| + rules := map[string]censusRule{} |
| + for _, entry := range entries { |
| + rule := censusRule{} |
| + rel := entry |
| + for _, tag := range []string{censusFileTag, censusTreeTag, censusLinkTag} { |
| + if rest, ok := strings.CutPrefix(entry, tag); ok { |
| + rule.kind, rel = tag, rest |
| + break |
| + } |
| + } |
| + if rule.kind == censusLinkTag { |
| + var ok bool |
| + rel, rule.target, ok = strings.Cut(rel, "\x00") |
| + if !ok || !filepath.IsAbs(rule.target) || filepath.Clean(rule.target) != rule.target || strings.ContainsRune(rule.target, 0) || censusNamesHost(rule.target, host) { |
| + return nil, ErrTypedInventory |
| + } |
| + } |
| + if !managedName(rel) || censusNamesHost(rel, host) { |
| + return nil, ErrTypedInventory |
| + } |
| + if old, ok := rules[rel]; ok && old != rule { |
| + return nil, ErrTypedInventory |
| + } |
| + rules[rel] = rule |
| + } |
| + return rules, nil |
| +} |
| + |
| +func (r censusRule) check(path string, d fs.DirEntry) error { |
| + symlink := d.Type()&os.ModeSymlink != 0 |
| + switch r.kind { |
| + case "": |
| + if symlink { |
| + return ErrTypedInventory |
| + } |
| + if d.IsDir() { |
| + return filepath.SkipDir |
| + } |
| + return nil |
| + case censusFileTag: |
| + if !d.Type().IsRegular() { |
| + return ErrTypedInventory |
| + } |
| + return nil |
| + case censusTreeTag: |
| + if symlink || !d.IsDir() { |
| + return ErrTypedInventory |
| + } |
| + return nil |
| + case censusLinkTag: |
| + if !symlink { |
| + return ErrTypedInventory |
| + } |
| + target, err := os.Readlink(path) |
| + if err != nil { |
| + return err |
| + } |
| + if target != r.target { |
| + return ErrTypedInventory |
| + } |
| + return nil |
| + } |
| + return ErrTypedInventory |
| +} |
| + |
| +// censusInTree reports whether the nearest classified ancestor of rel is a |
| +// shared tree. An opaque ancestor is skipped before its contents are read. |
| +func censusInTree(rules map[string]censusRule, rel string) bool { |
| + for dir := filepath.Dir(rel); dir != "." && dir != "/"; dir = filepath.Dir(dir) { |
| + if rule, ok := rules[filepath.ToSlash(dir)]; ok { |
| + return rule.kind == censusTreeTag |
| + } |
| + } |
| + return false |
| +} |
| + |
| +func censusNamesHost(s, host string) bool { |
| + return strings.Contains(s, host) || strings.Contains(s, strings.ReplaceAll(host, "-", `\x2d`)) |
| +} |
| + |
| func (c EmptyInstallationCensus) Initialize(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error { |
| if err := c.CheckLocal(ctx, obs, selected); err != nil { |
| return err |
| @@ -9138,36 +10045,38 @@ func (r *ColdRows) UnmarshalJSON(body []byte) error { |
| ```diff |
| --- a/internal/registry/registry.go |
| +++ b/internal/registry/registry.go |
| -@@ -35,0 +36,3 @@ |
| -+ HoldOp string |
| +@@ -36,0 +37,2 @@ |
| + ClaimOwnership |
| + |
| -@@ -437 +440 @@ |
| +@@ -446 +448 @@ |
| - "PRAGMA synchronous=NORMAL", |
| + "PRAGMA synchronous=FULL", |
| -@@ -595,0 +599,3 @@ |
| +@@ -605,0 +608,3 @@ |
| + if err := r.migrateClaimOwnership(); err != nil { |
| + return err |
| + } |
| -@@ -764 +770 @@ |
| --args, idle_seconds, max_run_seconds, replicas, replica` |
| +@@ -764 +769 @@ |
| +-// share \u2014 43 names, in this exact order. scanRoute's destinations, Upsert's |
| ++// share \u2014 47 names, in this exact order. scanRoute's destinations, Upsert's |
| +@@ -774 +779 @@ |
| +-args, idle_seconds, max_run_seconds, replicas, replica, hold_op` |
| +args, idle_seconds, max_run_seconds, replicas, replica, claim_incarnation, claim_stamp, claim_owner, claim_row_id, hold_op` |
| -@@ -775 +781 @@ |
| -- &t.Args, &t.IdleSeconds, &t.MaxRunSeconds, &t.Replicas, &rep) |
| +@@ -785 +790 @@ |
| +- &t.Args, &t.IdleSeconds, &t.MaxRunSeconds, &t.Replicas, &rep, &t.HoldOp) |
| + &t.Args, &t.IdleSeconds, &t.MaxRunSeconds, &t.Replicas, &rep, &t.ClaimIncarnation, &t.ClaimStamp, &t.ClaimOwner, &t.ClaimRowID, &t.HoldOp) |
| -@@ -988 +994 @@ |
| +@@ -1004 +1009 @@ |
| - _, err := r.sql.Exec(` |
| + res, err := r.sql.Exec(` |
| -@@ -990 +996 @@ |
| --VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |
| +@@ -1006 +1011 @@ |
| +-VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |
| +VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |
| -@@ -1016 +1022,3 @@ |
| +@@ -1034 +1039,3 @@ |
| - max_run_seconds=CASE WHEN excluded.max_run_seconds = 0 THEN routes.max_run_seconds ELSE excluded.max_run_seconds END`, |
| + max_run_seconds=CASE WHEN excluded.max_run_seconds = 0 THEN routes.max_run_seconds ELSE excluded.max_run_seconds END |
| +WHERE routes.claim_incarnation=excluded.claim_incarnation AND routes.claim_stamp=excluded.claim_stamp |
| +AND routes.claim_owner=excluded.claim_owner AND routes.claim_row_id=excluded.claim_row_id`, |
| -@@ -1024 +1032,8 @@ |
| -- t.Replicas, b2i(t.Replica)) |
| +@@ -1042 +1049,8 @@ |
| +- t.Replicas, b2i(t.Replica), t.HoldOp) |
| + t.Replicas, b2i(t.Replica), t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID, t.HoldOp) |
| + if err != nil { |
| + return err |
| @@ -9176,16 +10085,50 @@ func (r *ColdRows) UnmarshalJSON(body []byte) error { |
| + if err == nil && n != 1 { |
| + return ErrClaimOwnership |
| + } |
| -@@ -2216 +2231 @@ |
| -- db_path, region, fleet_box, addrs, health, gate |
| -+ db_path, region, fleet_box, addrs, health, gate, claim_incarnation, claim_stamp, claim_owner, claim_row_id |
| -@@ -2225,0 +2241 @@ |
| +@@ -2365 +2379,2 @@ |
| +- db_path, region, fleet_box, addrs, health, gate, hold_op |
| ++ db_path, region, fleet_box, addrs, health, gate, hold_op, |
| ++ claim_incarnation, claim_stamp, claim_owner, claim_row_id |
| +@@ -2374,0 +2390 @@ |
| + var claim ClaimOwnership |
| -@@ -2228 +2244 @@ |
| -- &channel, &backing, &tunnel, &dbPath, ®ion, &fleetBox, &addrs, &health, &gate); err != nil { |
| -+ &channel, &backing, &tunnel, &dbPath, ®ion, &fleetBox, &addrs, &health, &gate, &claim.ClaimIncarnation, &claim.ClaimStamp, &claim.ClaimOwner, &claim.ClaimRowID); err != nil { |
| -@@ -2233,0 +2250 @@ |
| -+ fmt.Fprintf(h, "%s\x1f%d\x1f%s\x1f%s\x1e", claim.ClaimIncarnation, claim.ClaimStamp, claim.ClaimOwner, claim.ClaimRowID) |
| +@@ -2377 +2393,2 @@ |
| +- &channel, &backing, &tunnel, &dbPath, ®ion, &fleetBox, &addrs, &health, &gate, &holdOp); err != nil { |
| ++ &channel, &backing, &tunnel, &dbPath, ®ion, &fleetBox, &addrs, &health, &gate, &holdOp, |
| ++ &claim.ClaimIncarnation, &claim.ClaimStamp, &claim.ClaimOwner, &claim.ClaimRowID); err != nil { |
| +@@ -2388,0 +2406,15 @@ |
| ++ } |
| ++ // Ownership follows the same rule: the all-default value is omitted, |
| ++ // and each non-default field is tagged and quoted so a partial value |
| ++ // cannot alias another field's. |
| ++ if claim.ClaimIncarnation != "" { |
| ++ fmt.Fprintf(h, "\x1fclaim_incarnation=%q", claim.ClaimIncarnation) |
| ++ } |
| ++ if claim.ClaimStamp != 0 { |
| ++ fmt.Fprintf(h, "\x1fclaim_stamp=%d", claim.ClaimStamp) |
| ++ } |
| ++ if claim.ClaimOwner != "" { |
| ++ fmt.Fprintf(h, "\x1fclaim_owner=%q", claim.ClaimOwner) |
| ++ } |
| ++ if claim.ClaimRowID != "" { |
| ++ fmt.Fprintf(h, "\x1fclaim_row_id=%q", claim.ClaimRowID) |
| +@@ -2557,0 +2590,8 @@ |
| ++// snapshotClaimCols are the ownership columns, hashed only when non-default. |
| ++var snapshotClaimCols = map[string]bool{ |
| ++ "claim_incarnation": true, |
| ++ "claim_stamp": true, |
| ++ "claim_owner": true, |
| ++ "claim_row_id": true, |
| ++} |
| ++ |
| +@@ -2596,0 +2637,8 @@ |
| ++ } |
| ++ continue |
| ++ } |
| ++ // Claim ownership columns follow the hold_op rule: default |
| ++ // values are omitted, anything else is tagged and quoted. |
| ++ if snapshotClaimCols[cols[i]] { |
| ++ if v.Valid && v.String != "" && (cols[i] != "claim_stamp" || v.String != "0") { |
| ++ fmt.Fprintf(h, "%s=%q\x1f", cols[i], v.String) |
| ``` |
| #### Patch: `internal/registry/timers.go` |
| @@ -10019,7 +10962,7 @@ func (s *Server) handleHostWakeRetryReset(w http.ResponseWriter, r *http.Request |
| ```diff |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| -@@ -474,4 +474,5 @@ |
| +@@ -478,4 +478,5 @@ |
| - HostClaim bool |
| - DBVersions lease.VersionedStore |
| - Origin string // the console's public origin \u2014 the OAuth client_id |
| @@ -10970,11 +11913,6 @@ func refusingRootWake(box string, store release.Store) *activator.ClaimRuntime { |
| return activator.ClaimStart{}, hostwrite.ErrUnavailable |
| }, FailedAdmission: func(context.Context, hostclaim.Observed, error) error { return hostwrite.ErrUnavailable }} |
| } |
| - |
| -func claimGlobalSharedPaths(r ClaimRuntimeResources) map[string][]string { |
| - shared := map[string][]string{"data": {".hostclaims", ".hostdir", "hostguard", "registry.db", "registry.db-wal", "registry.db-shm"}, "opt": {"system", "releases"}, "units": {"carlos-edge.service", "carlos-adopt.service", "carlos-adopt.timer", "carlos-instance-control.service", "carlos-instance-control.socket", "carlos-stage.service", "carlos-stage.path", "carlos-restart.service", "carlos-restart.path", "carlos-config-sync.service", "carlos-system-update.service", "carlos-system-update.path", "carlos-system-recover.service"}} |
| - return shared |
| -} |
| ``` |
| #### New file: `internal/host/claimhandover_transport.go` |
| @@ -14395,6 +15333,395 @@ func RunWithClaim(args []string, handler http.Handler) { runWithClaim(args, hand |
| + host.RunInstanceControl(args) |
| ``` |
| +#### Patch: `internal/host/bootstrap.go` |
| + |
| +```diff |
| +--- a/internal/host/bootstrap.go |
| ++++ b/internal/host/bootstrap.go |
| +@@ -16,0 +17 @@ |
| ++ "errors" |
| +@@ -369,0 +371,13 @@ |
| ++ hostEnv := filepath.Join(root, "etc", "carlos", "host.env") |
| ++ if _, err := os.Stat(hostEnv); errors.Is(err, os.ErrNotExist) { |
| ++ hostEnv = claimBootstrapHostEnv |
| ++ } |
| ++ claimSettings, err := readClaimUnitSettings(hostEnv) |
| ++ if err != nil { |
| ++ return err |
| ++ } |
| ++ beforeControl, err := os.ReadFile(filepath.Join(root, "etc", "systemd", "system", claimControlUnit)) |
| ++ if err != nil && !errors.Is(err, os.ErrNotExist) { |
| ++ return err |
| ++ } |
| ++ var afterControl []byte |
| +@@ -505,0 +520,3 @@ |
| ++ if name == claimControlUnit { |
| ++ afterControl = claimUnitContent(name, content, claimSettings) |
| ++ } |
| +@@ -507 +524 @@ |
| +- if err := os.WriteFile(path, content, 0o644); err != nil { |
| ++ if err := os.WriteFile(path, claimUnitContent(name, content, claimSettings), 0o644); err != nil { |
| +@@ -585,0 +603,3 @@ |
| ++ } |
| ++ if err := restartClaimControl(filepath.Join(unitDir, claimControlUnit), beforeControl, afterControl, func(args ...string) (string, error) { return "", systemctl(args...) }); err != nil { |
| ++ return err |
| +``` |
| + |
| +#### New file: `internal/host/claimcensus.go` |
| + |
| +```go |
| +package host |
| + |
| +import ( |
| + "bufio" |
| + "bytes" |
| + "path/filepath" |
| + "slices" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// claimGlobalSharedPaths is the empty-installation census's classification |
| +// of what the box's own installers put under the managed roots (host |
| +// lifecycle claim P6-D2: shared release directories, shared unit templates |
| +// and the registry database are not host-owned). Each entry is derived from |
| +// the installer that writes it: bootstrap's embedded unit set and arming |
| +// plan, the self-update applier's opt layout and the release store's |
| +// version layout. Nothing here is a wildcard. Per-host live links, unit |
| +// instances, drop-ins and env files stay unclassified, so the census |
| +// refuses them. |
| +func claimGlobalSharedPaths(r ClaimRuntimeResources) map[string][]string { |
| + return map[string][]string{ |
| + "data": claimDataCensus(r.DataDir), |
| + "opt": claimOptCensus(r.OptDir), |
| + "units": claimUnitCensus(r.UnitsDir), |
| + } |
| +} |
| + |
| +// claimCensusRel names path below root, or "" (which the census refuses as |
| +// a malformed classification) when it is not strictly inside it. |
| +func claimCensusRel(root, path string) string { |
| + rel, err := filepath.Rel(root, path) |
| + if err != nil || !filepath.IsLocal(rel) { |
| + return "" |
| + } |
| + return filepath.ToSlash(rel) |
| +} |
| + |
| +func claimDataCensus(dataDir string) []string { |
| + // The claim journals and guard records verify their own contents; the |
| + // census reads neither. The registry is bootstrap's file, shared by |
| + // every host, plus SQLite's sidecars once something has opened it. |
| + reg := claimCensusRel(dataDir, filepath.Join(dataDir, "registry.db")) |
| + return []string{".hostclaims", "hostguard", hostbox.CensusFile(reg), hostbox.CensusFile(reg + "-wal"), hostbox.CensusFile(reg + "-shm")} |
| +} |
| + |
| +func claimOptCensus(optDir string) []string { |
| + entries := []string{ |
| + // The staged platform binary and the self-update applier's fallback |
| + // with its sidecars. |
| + hostbox.CensusFile(claimCensusRel(optDir, systemBinaryPath(optDir))), |
| + hostbox.CensusFile(claimCensusRel(optDir, systemPrevPath(optDir))), |
| + hostbox.CensusFile(claimCensusRel(optDir, prevVersionPath(optDir))), |
| + hostbox.CensusFile(claimCensusRel(optDir, prevAllowedPath(optDir))), |
| + // The shared release store: <opt>/versions/<ver>/[<acct>/]<app>. |
| + // Directories and regular files only; a symlink in it is refused. |
| + hostbox.CensusTree(claimCensusRel(optDir, filepath.Dir(filepath.Dir(release.VersionPath(optDir, "", "v", "a"))))), |
| + } |
| + // Root's self-update state and the host-floor guard copy. Both are box |
| + // state below one parent; it is opaque to the census, as before. |
| + for _, dir := range []string{systemRootDir(optDir), systemGuardDir(optDir)} { |
| + if rel := claimCensusRel(optDir, filepath.Dir(dir)); !slices.Contains(entries, rel) { |
| + entries = append(entries, rel) |
| + } |
| + } |
| + // <opt>/live itself is only a directory. Every link in it belongs to a |
| + // host and is unclassified. |
| + return entries |
| +} |
| + |
| +// claimCensusEnabledUnits is every unit an installer enables: bootstrap's |
| +// arming plan (which a self-update apply also arms, from the target build's |
| +// own plan) and ensureSystemUnits' second belt. |
| +func claimCensusEnabledUnits() []string { |
| + units := currentBootstrapPlan().Enabled |
| + for _, u := range []string{systemUpdatePathUnit, systemRecoverUnit, instanceControlSocket} { |
| + if !slices.Contains(units, u) { |
| + units = append(units, u) |
| + } |
| + } |
| + return units |
| +} |
| + |
| +func claimUnitCensus(unitsDir string) []string { |
| + names, err := unitNames() |
| + if err != nil { |
| + // Nothing of the platform's is classified, so the census refuses. |
| + return nil |
| + } |
| + var entries []string |
| + for _, name := range names { |
| + // Every embedded unit, templates, slices and mounts included, is a |
| + // file bootstrap and the applier install for the whole box. |
| + entries = append(entries, hostbox.CensusFile(name)) |
| + } |
| + for _, unit := range claimCensusEnabledUnits() { |
| + if !slices.Contains(names, unit) || strings.Contains(unit, "@") { |
| + continue |
| + } |
| + body, err := unitsFS.ReadFile("units/" + unit) |
| + if err != nil { |
| + continue |
| + } |
| + target := filepath.Join(unitsDir, unit) |
| + for _, link := range claimUnitInstallLinks(unit, body) { |
| + entries = append(entries, hostbox.CensusLink(link, target)) |
| + } |
| + } |
| + return entries |
| +} |
| + |
| +// claimUnitInstallLinks lists the symlinks `systemctl enable` creates for a |
| +// non-template unit from its [Install] section, relative to the unit |
| +// directory. A value it cannot read exactly (a specifier, an Also= list) is |
| +// left out, so the link it would have made stays unclassified. |
| +func claimUnitInstallLinks(unit string, body []byte) []string { |
| + var links []string |
| + section := "" |
| + scan := bufio.NewScanner(bytes.NewReader(body)) |
| + for scan.Scan() { |
| + line := strings.TrimSpace(scan.Text()) |
| + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { |
| + continue |
| + } |
| + if strings.HasPrefix(line, "[") { |
| + section = line |
| + continue |
| + } |
| + key, value, ok := strings.Cut(line, "=") |
| + if section != "[Install]" || !ok { |
| + continue |
| + } |
| + suffix := map[string]string{"WantedBy": ".wants/", "RequiredBy": ".requires/", "UpheldBy": ".upholds/", "Alias": ""} |
| + dir, known := suffix[strings.TrimSpace(key)] |
| + if !known { |
| + continue |
| + } |
| + for _, name := range strings.Fields(value) { |
| + if strings.ContainsAny(name, "%/\\") { |
| + continue |
| + } |
| + if dir == "" { |
| + links = append(links, name) |
| + continue |
| + } |
| + links = append(links, name+dir+unit) |
| + } |
| + } |
| + return links |
| +} |
| +``` |
| + |
| +#### New file: `internal/host/claimunit.go` |
| + |
| +```go |
| +package host |
| + |
| +import ( |
| + "bufio" |
| + "bytes" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "strconv" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| +) |
| + |
| +const claimControlUnit = "carlos-instance-control.service" |
| + |
| +var claimBootstrapHostEnv = "/etc/carlos/host.env" |
| + |
| +const claimControlEnvironment = "EnvironmentFile=-/etc/carlos/host.env\n" |
| + |
| +type claimUnitSettings struct { |
| + enabled bool |
| + data, opt string |
| +} |
| + |
| +func readClaimUnitSettings(path string) (claimUnitSettings, error) { |
| + values := map[string]string{"CARLOS_HOST_CLAIM": "", "CARLOS_DATA_DIR": "", "CARLOS_OPT_DIR": ""} |
| + raw, err := os.ReadFile(path) |
| + if err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return claimUnitSettings{}, err |
| + } |
| + if err == nil { |
| + // The installed EnvironmentFile is authoritative, including absent keys. |
| + // Never execute it as shell code. Refuse unsupported syntax for these |
| + // three settings rather than render permissions for a different value. |
| + for key := range values { |
| + values[key] = "" |
| + } |
| + scan := bufio.NewScanner(bytes.NewReader(raw)) |
| + for scan.Scan() { |
| + line := strings.TrimSpace(scan.Text()) |
| + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { |
| + continue |
| + } |
| + key, v, ok := strings.Cut(line, "=") |
| + key = strings.TrimSpace(key) |
| + v = strings.TrimSpace(v) |
| + if !ok || strings.HasSuffix(v, `\`) || (len(v) > 0 && (v[0] == '"' || v[0] == '\'') && (len(v) < 2 || v[len(v)-1] != v[0])) { |
| + return claimUnitSettings{}, errors.New("unsupported multiline host unit setting") |
| + } |
| + if _, known := values[key]; !known { |
| + continue |
| + } |
| + v = strings.TrimSpace(v) |
| + if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) { |
| + v = v[1 : len(v)-1] |
| + } |
| + if strings.ContainsAny(v, "\\\"'\x00\r\n") { |
| + return claimUnitSettings{}, fmt.Errorf("unsupported host unit setting %s", key) |
| + } |
| + values[key] = v |
| + } |
| + if err := scan.Err(); err != nil { |
| + return claimUnitSettings{}, err |
| + } |
| + } |
| + cfg := claimUnitSettings{enabled: hostwrite.Enabled(func(k string) string { return values[k] }), data: values["CARLOS_DATA_DIR"], opt: values["CARLOS_OPT_DIR"]} |
| + if cfg.data == "" { |
| + cfg.data = "/data" |
| + } |
| + if cfg.opt == "" { |
| + cfg.opt = "/opt/carlos" |
| + } |
| + if cfg.enabled { |
| + for _, p := range []string{cfg.data, cfg.opt} { |
| + if !filepath.IsAbs(p) || filepath.Clean(p) != p || p == "/" || strings.ContainsAny(p, "\x00\r\n") { |
| + return claimUnitSettings{}, errors.New("invalid claim unit root") |
| + } |
| + } |
| + } |
| + return cfg, nil |
| +} |
| + |
| +func claimUnitContent(name string, content []byte, cfg claimUnitSettings) []byte { |
| + if name != claimControlUnit || !cfg.enabled { |
| + return content |
| + } |
| + quote := func(p string) string { |
| + if !strings.ContainsAny(p, " \t%\\\"") { |
| + return p |
| + } |
| + return strconv.Quote(strings.ReplaceAll(p, "%", "%%")) |
| + } |
| + roots := []string{"/run/carlos-control", "/run/carlos", cfg.data, cfg.opt, "/etc/systemd/system", "/sys/fs/cgroup"} |
| + for i, p := range roots { |
| + roots[i] = quote(p) |
| + } |
| + content = bytes.Replace(content, []byte("[Service]\n"), []byte("[Service]\n"+claimControlEnvironment), 1) |
| + content = bytes.Replace(content, []byte("ReadWritePaths=/run/carlos-control\n"), []byte("ReadWritePaths="+strings.Join(roots, " ")+"\n"), 1) |
| + return bytes.Replace(content, []byte("RestrictAddressFamilies=AF_UNIX\n"), []byte("RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n"), 1) |
| +} |
| + |
| +func claimControlChanged(before, after []byte) bool { |
| + return !bytes.Equal(before, after) && (bytes.Contains(before, []byte(claimControlEnvironment)) || bytes.Contains(after, []byte(claimControlEnvironment))) |
| +} |
| + |
| +func restartClaimControl(path string, before, after []byte, systemctl func(...string) (string, error)) error { |
| + if !claimControlChanged(before, after) { |
| + return nil |
| + } |
| + if _, err := systemctl("try-restart", claimControlUnit); err != nil { |
| + // Restore the old bytes so the next repair still sees a change. |
| + var restore error |
| + if before == nil { |
| + restore = os.Remove(path) |
| + } else { |
| + restore = writeFileDurable(path, before, 0644) |
| + } |
| + _, reload := systemctl("daemon-reload") |
| + return errors.Join(err, restore, reload) |
| + } |
| + return nil |
| +} |
| +``` |
| + |
| +#### Patch: `internal/host/systemapply.go` |
| + |
| +```diff |
| +--- a/internal/host/systemapply.go |
| ++++ b/internal/host/systemapply.go |
| +@@ -1185,0 +1186,4 @@ |
| ++ beforeControl, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) |
| ++ if err != nil && !errors.Is(err, os.ErrNotExist) { |
| ++ return err |
| ++ } |
| +@@ -1247,2 +1251,8 @@ |
| +- _, err = a.ops.systemctl("daemon-reload") |
| +- return err |
| ++ if _, err = a.ops.systemctl("daemon-reload"); err != nil { |
| ++ return err |
| ++ } |
| ++ afterControl, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) |
| ++ if err != nil && !errors.Is(err, os.ErrNotExist) { |
| ++ return err |
| ++ } |
| ++ return restartClaimControl(filepath.Join(unitDir, claimControlUnit), beforeControl, afterControl, a.ops.systemctl) |
| +``` |
| + |
| +#### Patch: `internal/host/systemunits.go` |
| + |
| +```diff |
| +--- a/internal/host/systemunits.go |
| ++++ b/internal/host/systemunits.go |
| +@@ -3,0 +4 @@ |
| ++ "bytes" |
| +@@ -61,0 +63,4 @@ |
| ++ claimSettings, settingsErr := readClaimUnitSettings(filepath.Join(etcDir, "carlos", "host.env")) |
| ++ if settingsErr != nil { |
| ++ logf("adopt: instance-control unit settings: %v", settingsErr) |
| ++ } |
| +@@ -91,0 +97,3 @@ |
| ++ if u == claimControlUnit && settingsErr != nil { |
| ++ continue |
| ++ } |
| +@@ -96 +104,12 @@ |
| +- if !missing && *isActive && isEnabled != nil && *isEnabled && *ctlActive { |
| ++ control, err := unitsFS.ReadFile("units/" + claimControlUnit) |
| ++ if err != nil { |
| ++ logf("adopt: system units: %v", err) |
| ++ return |
| ++ } |
| ++ installed, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit)) |
| ++ if err != nil && !os.IsNotExist(err) { |
| ++ logf("adopt: instance-control unit: %v", err) |
| ++ return |
| ++ } |
| ++ controlChanged := settingsErr == nil && (err != nil || !bytes.Equal(installed, claimUnitContent(claimControlUnit, control, claimSettings))) |
| ++ if !missing && !controlChanged && *isActive && isEnabled != nil && *isEnabled && *ctlActive { |
| +@@ -103,0 +123,3 @@ |
| ++ if u == claimControlUnit && settingsErr != nil { |
| ++ continue |
| ++ } |
| +@@ -109 +131 @@ |
| +- if err := writeFileDurable(filepath.Join(unitDir, u), b, 0o644); err != nil { |
| ++ if err := writeFileDurable(filepath.Join(unitDir, u), claimUnitContent(u, b, claimSettings), 0o644); err != nil { |
| +@@ -116,0 +139,6 @@ |
| ++ return |
| ++ } |
| ++ } |
| ++ if settingsErr == nil { |
| ++ if err := restartClaimControl(filepath.Join(unitDir, claimControlUnit), installed, claimUnitContent(claimControlUnit, control, claimSettings), systemctl); err != nil { |
| ++ logf("adopt: system units: %v", err) |
| +``` |
| + |
| ## Task 8: connect scheduled delivery admission |
| The claim-enabled runner requires ClaimDeliverer and DeliveryAdmission. Missing providers cannot fall through to ordinary HTTP. The application's scheduled handler requests admission after wake, propagates producer cancellation to the backend request, and joins before bookkeeping. Flag-off uses the existing delivery path. The guarded timer transaction keeps the original registration and selection throughout. |
| @@ -14482,15 +15809,16 @@ Apply all implementation blocks after the tests-first failure. These tasks share |
| python3 "$TMPDIR/claim6b-apply.py" docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md implementation |
| gofmt -l . |
| nice -n 10 go test -p 1 ./internal/cgroup ./internal/hostguard ./internal/registry ./internal/hostbox ./internal/activator ./internal/host ./internal/provision ./internal/console ./internal/instancectl ./internal/instanceexec ./internal/schedule -run '^Test(Claim|ServingRetries|Renewing|AggregateContext|Identity|AdapterSelectionLayout|Launch|Prelaunch|PreLaunch|Recover|EmptyCensus|RegistryRows|ColdHandover|HostWakeReset|RootRetirement|BeginFence|Cleanup|Fence|StartAdmitted|SameAdmission|SetupAt|RunAdoptDisabledClaims|RunAdoptOpensTheRegistryThroughTheHelper)' -count=1 |
| +nice -n 10 go test -p 1 ./internal/host ./internal/hostbox ./internal/registry ./internal/activator ./internal/provision ./internal/instancectl ./internal/cgroup ./internal/schedule ./internal/hostguard ./internal/console ./internal/instanceexec ./internal/park . -count=1 -timeout=30m |
| nice -n 10 go build -p 1 ./... |
| nice -n 10 go vet -p 1 ./... |
| -nice -n 10 go test -p 1 ./... -count=1 |
| +nice -n 10 go test -p 1 ./... -count=1 -timeout=30m |
| GOOS=darwin GOARCH=amd64 nice -n 10 go build -p 1 ./... |
| GOOS=windows GOARCH=amd64 nice -n 10 go build -p 1 ./... |
| GOOS=linux GOARCH=arm64 nice -n 10 go build -p 1 ./... |
| ``` |
| -Use the default Go build cache. For this authoring session only, the user authorized `GOCACHE=/home/paulca/.cache/tmp/claude-1001/own/gocache-6b5` if the default is read-only; remove that fallback cache when done. Put scratch under `/home/paulca/.cache/tmp/claude-1001/` and remove it after verification. Run Go gates serially with `nice -n 10` and `-p 1`. An exported source tree without Git metadata needs `-buildvcs=false`; that flag changes build stamping, not the code being checked. |
| +Use the default Go build cache. For this authoring session only, the user authorized `GOCACHE=/home/paulca/.cache/tmp/claude-1001/own/gocache-6b6` if the default is read-only; remove that fallback cache when done. Put scratch under `/home/paulca/.cache/tmp/claude-1001/` and remove it after verification. Run Go gates serially with `nice -n 10` and `-p 1`. An exported source tree without Git metadata needs `GOFLAGS="-buildvcs=false -p=1"` so child `go build` commands inherit the same stamping and serial-build settings. Use a canonical disk-backed `TMPDIR`. Some existing socket tests require a shorter path than this machine provides; record those failures against unmodified main rather than introduce a symlink ancestor that the containment tests correctly reject. The console suite needs the extended timeout shown above. These settings change the test environment, not the code being checked. |
| Review the resulting diff against the parent, including actual callers. The repository requires authority/production review by Opus before implementation lands. Plan authorship and local compilation are not that review. Land the eventual implementation only through the reviewed amadan branch and its green exact-tip gate. This document does not authorize a production rollout or infrastructure change. |
| @@ -14512,6 +15840,12 @@ Do not substitute passing filesystem fixtures for these kernel witnesses. No cur |
| ## Self-review |
| +Rev 6 integration check: |
| + |
| +- Default ownership contributes no bytes to either hash. Each populated field, including a string value of `0`, changes both hashes; the golden regression from main remains unchanged. |
| +- The unit fixture verifies bootstrap, apply and already-armed repair in both modes, host.env precedence, configured roots and restart retry. A real systemd sandbox remains a rollout witness. |
| +- The census fixture contains the platform binary and fallback, staged versions, every embedded unit and all installer-enabled links. Changed targets, host live links, unknown files and a durable fence refuse without acquiring ownership. Unknown non-platform units also remain unclassified; classifying unrelated local installations belongs to backfill and is not inferred from a name or a text search. |
| + |
| Rev 5 integration check: |
| - The ownership regression follows the adoption wrappers to the implementation, requires `openServiceRegistry`, and refuses direct `registry.Open` in every function in that chain. Both focused commands include it. |
| @@ -14544,21 +15878,30 @@ Rev 4 integration check: |
| ### External completion gates |
| -The reservation-bearing extension of main's isolated root launch protocol is required before any tenant shape can run with claims enabled. It must preserve main's namespace isolation and bind every supported role to the durable reservation; it is not supplied by the external owner adapters. Other implementation dependencies are: Plan 8's verified controller/deadline/quarantine adapter; Plan 8b's signed roster, floor and running-image adapter; Plan 5's positive empty/backfill ownership authority; and moves' held-input selection, operation attempts, timer replacement, marker publication and complete database/files live view. The default production binding refuses until those typed adapters are installed. Region HA remains a separate supported-placement gate. |
| +The census classifies only the platform artifacts produced by the repo installers. Unclassified distro or local units in `/etc/systemd/system` still refuse initialization and require an explicitly reviewed inventory/backfill boundary; no name or contents heuristic supplies that authority. A populated bootstrap fixture does not establish readiness on a deployed box. The reservation-bearing extension of main's isolated root launch protocol is required before any tenant shape can run with claims enabled. It must preserve main's namespace isolation and bind every supported role to the durable reservation; it is not supplied by the external owner adapters. Other implementation dependencies are: Plan 8's verified controller/deadline/quarantine adapter; Plan 8b's signed roster, floor and running-image adapter; Plan 5's positive empty/backfill ownership authority; and moves' held-input selection, operation attempts, timer replacement, marker publication and complete database/files live view. The default production binding refuses until those typed adapters are installed. Region HA remains a separate supported-placement gate. |
| ### Authoring verification |
| -Rev 5: all 122 file blocks were replayed with this document's extractor into a fresh on-disk export of `origin/main` at `3407d114f8c8a413214b88295457263bea50dcb7`, the latest main observed when preparing the final run. The replayed files match the source used for verification byte for byte. Main advanced during this revision: the refreshed patches preserve GeoIP and release-proof work, the signed-floor host-resolution dispatcher from `cbc0922e`, and the console/backfill work from `3407d114`. The instance loop retains main's write policy and backfill callback alongside the provisioning configurator. Whole-tree `gofmt -l .` and the plan's whitespace check passed. Only this plan changed in the design worktree; no branch, index or commit was written, and no other plan was edited. |
| +Rev 6: all 130 file blocks replayed into a clean on-disk export and match the tested files byte for byte. The final pinned base is `9cf9f18f53aac403f49ad755089c41c490b7e735`, the latest `origin/main` observed at the final check. The full package runs began on `8e9ac1b8471dc10ba63921357e4e179307fb3f4d`. Main advanced during those runs by two files in `internal/stopevidence` only. `go list -deps` confirmed that none of the twelve internal packages below, or the root package, imports that package, directly or transitively. Their tested source and dependency graphs are unchanged on the final base. The final replay, the full `internal/stopevidence` suite and every repository-wide build gate were rerun after refreshing the base. No main change was dropped or overwritten. |
| + |
| +Tests-first replay failed on the expected missing implementation APIs. Main's unchanged `TestFingerprintsUnchangedForUnheldRows` passed in the full registry suite; restoring unconditional ownership bytes made that golden regression fail. The new fingerprint cases cover every field independently, partial ownership, the string `0`, distinct fields with the same value, and restoration of the default hashes. Disabling the conditional unit renderer made the installer regression fail. Restoring rev 5's census classification made the populated bootstrap fixture fail at first installation on canonical disk paths; the final classification passes. The fixture runs bootstrap, staged system-unit installation and arming, the system-unit repair path, and release staging. It includes the platform binary, its fallback and sidecars, version directories, embedded units and enablement links. Substituted targets, unknown files or units, host live links, and durable fences refuse without acquiring ownership. |
| + |
| +All Go test/build commands ran serially under `nice -n 10`, with `-p 1`. Full package runs had no `-run` filter: |
| + |
| +- Passed: `internal/host`, `internal/hostbox`, `internal/registry`, `internal/provision`, `internal/instancectl`, `internal/cgroup`, `internal/schedule`, `internal/hostguard`, `internal/console`, and the additionally touched `internal/park`. |
| +- The final full host run passed after adding the test harness that isolates bootstrap's host.env fallback from the developer machine. The full console run completed in 716.7 seconds with 2,362 passing tests and one skip, using `-timeout=30m`. |
| +- `internal/activator` and the additionally touched `internal/instanceexec` completed with the baseline failures listed below. The extra root-package command aborted with the baseline fatal error below; tests after that fatal did not run. |
| +- On the final base, `go build ./...`, `go vet ./...`, and `go test ./... -run '^$' -count=1` passed. The last command compiles every test package, rather than executing every repository test. The complete newly changed `internal/stopevidence` package also passed. |
| +- Darwin/amd64, Windows/amd64 and Linux/arm64 builds passed on the final base. Whole-tree `gofmt -l .` and the plan's whitespace check passed. |
| -Tests-first replay produced the expected missing-API compilation failures. Before the round-4 fixes, `TestRunAdoptOpensTheRegistryThroughTheHelper` failed because its source inspection stopped at the wrapper. The new `TestClaimProductionCensusInstallsThroughReconciler` failed at first installation with `hostbox: invalid typed retirement inventory` while the production list still named `.hostguard`. Both pass with the corrected blocks. The census test uses the real reconciler, guard, work selector, registry and durable inventory with the production shared-path list. It verifies first installation, repeat ownership, refusal of an unknown data file before activation, and refusal of a durable fence with its bytes unchanged and no route or inventory written. Its managed roots are disposable directories, not a census of a deployed box. |
| +Failures reproduced separately on unmodified main: |
| -Final gates ran serially under `nice -n 10`, with `-p 1` and `-buildvcs=false` on every Go command: |
| +- `internal/activator`: the control-socket tests exceed Linux's Unix-socket path limit under the canonical `TMPDIR=/home/paulca/.cache/tmp`. Both the replay and main fail `TestExecBackendStartPassesControlSocketInEnvAndStartsPurgeListener`, `TestExecBackendStartWithNilCacheStillBindsControlSocket`, `TestExecBackendStartMountsScheduleVerbsOnControlSocket`, `TestExecBackendStartWithNilTimersMountsNoScheduleVerbs`, `TestExecBackendStartTenantCannotClaimAbsentPurgeChannel`, and `TestExecBackendStartOverridesTenantSuppliedPurgeVars`. Main also hit the same length boundary in `TestExecBackendStartAdvertisesHeaderPurgeChannel`. |
| +- `internal/instanceexec`: `TestProbePassesOnAnInaccessibleControlNode` fails on both trees because the overlong control-socket path returns `EINVAL`, rather than the expected permission refusal. |
| +- Root package: `TestShipVerbAPIMode` calls `log.Fatal` after selecting the operator's global default console instead of its local test console. The replay and main abort at the same test with the same missing-login error. This is not a passing root suite; the fatal prevents the remaining tests from executing. No production request was authenticated. |
| -- Full `go test ./internal/host -count=1` and `go test ./internal/hostbox -count=1` passed, without a focused filter. |
| -- The focused command in Task 9 passed for all eleven packages. It includes the adoption ownership test, production census regression, earlier claim regressions and mounted wake-retry reset checks. The reset tests exercise the existing dispatcher, its signed-floor gate, malformed payload refusals and preservation of declare-lost/readmit dispatch. |
| -- `go build ./...`, `go vet ./...` and `go test ./... -run '^$' -count=1` passed. The last command compiles every test package; it is not full-repository test execution. |
| -- Darwin/amd64, Windows/amd64 and Linux/arm64 builds passed. |
| +The initial console run also exposed an export-specific child-build problem: passing `-buildvcs=false` only to the parent does not reach tests that invoke `go build`. That failure reproduced on main. The final run used `GOFLAGS="-buildvcs=false -p=1"` so child builds inherit both settings, and the full package passed. The initial default ten-minute timeout was too short for the full console suite and was replaced by the thirty-minute limit above. A temporary short symlink to disk-backed scratch was tried for socket lengths, but containment tests correctly reject symlink ancestors and other host tests compare canonical paths. That run is not counted as a pass. Final verification retained canonical disk paths and reports the socket-length failures rather than weakening those checks. |
| -An initial host-package run used a scratch path long enough to exceed Unix socket path limits. The final run used the shorter on-disk `TMPDIR=/home/paulca/.cache/tmp` and passed without changing those tests. The default Go cache was read-only, so verification used the brief's authorized `/home/paulca/.cache/tmp/claude-1001/own/gocache-6b5`. That cache and all revision-5 verification exports, logs and scratch files were removed after the gates. |
| +Opus reviewed the fingerprint encoding, census classification and root-unit configuration. Follow-up review confirmed that the unsafe foreign-unit heuristic was removed, missing host.env cannot inherit authority from the installer environment, bad settings leave the control unit untouched while allowing unrelated system-unit repair, active control services receive changed configuration, and restart failure remains retryable. The unit tests cover flag-off byte identity, enabled environment/network/write roots, host.env precedence, configured roots, repair and restart failure. Real systemd sandbox behavior and privileged kernel witnesses remain rollout checks; no skipped witness counts as evidence. Unclassified distro/local units still require the explicit inventory/backfill boundary recorded above. The external owner bindings and reservation-bearing isolated launch protocol remain activation gates. |
| -Opus reviewed the ownership check, shared-path correction, fence regression and resolution-dispatch integration; it found no blocker in the reset door or dispatcher join. Its broader questions about enabled startup, adoption ordering and real deployment-root classification remain part of the implementation and rollout review, not claims established by these filesystem fixtures. The existing privileged and opt-in kernel witnesses remain rollout gates; a package pass does not turn a skipped witness into evidence. No enabled tenant launch shape is supplied by this plan, and the reservation-bearing isolated root protocol remains required. Full-repository test execution, authority review and the exact-tip amadan gate remain requirements before implementation lands. No live deployment or infrastructure change was performed. |
| +Only this plan was edited in the design worktree. No branch, index, commit, deployment or infrastructure resource was written. The default Go cache was read-only, so the brief's authorized `/home/paulca/.cache/tmp/claude-1001/own/gocache-6b6` was used. Verification exports, logs, the short alias, this run's abandoned test fixtures and that fallback cache were removed after verification. Full-repository test execution without the baseline aborts, authority review of the eventual implementation, and green exact-tip amadan CI remain requirements before landing. |