hostfiles: refuse every over-cap entry up front; export the readers' cap rule as RestorableWithinCap (Paul's ruling, 2026-10-15)
Each attempt to tolerate an over-cap path in the readers opened a new edge. Astra's round 5 found two more: - dropping deep skipped entries from superseded() restores an obsolete ancestor file; - the scan's exclusion callback still walks a deep skipped path's ancestors. So no entry is exempt. The readers, Open, OpenAt, Commit and StageDelete are main's again, byte for byte. The only change is the exported RestorableWithinCap, which is manifestWithinCap: every entry, live, tombstoned or skipped, within the cap. Moves' cut preflight and the claim's files backfill (#250) call it, so a host with such a manifest is reported and blocked until an operator replaces it. Moves and the claim are off in production. Test: TestRestorableWithinCapIsTheReadersRule. The Plan 14 overlay is envelope-only. Astra's round 5 review is committed unedited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed,
+91
−229
docs/superpowers/plans/CODEX-REVIEW-HOSTFILES-CAP-5.md+31 −0internal/hostfiles/fix4_linux_test.go+6 −9internal/hostfiles/fix5_linux_test.go+27 −173internal/hostfiles/publisher.go+13 −22internal/hostfiles/restore.go+5 −12internal/hostfiles/sync.go+4 −7internal/provision/plan14_root_reads_test.go+3 −4internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv+2 −2
diff --git a/docs/superpowers/plans/CODEX-REVIEW-HOSTFILES-CAP-5.md b/docs/superpowers/plans/CODEX-REVIEW-HOSTFILES-CAP-5.md| new file mode 100644 |
| index 000000000..6461aa27f |
| --- /dev/null |
| +++ b/docs/superpowers/plans/CODEX-REVIEW-HOSTFILES-CAP-5.md |
| @@ -0,0 +1,31 @@ |
| +**Request changes: two P2 findings. HEAD `c29cc49f` is not ready to merge.** |
| + |
| +1. **P2 — Ignoring deep skipped entries can restore an obsolete ancestor file.** At [restore.go:226](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/hostfiles-cap-live-entries/internal/hostfiles/restore.go:226), the new guard drops over-cap entries from overlap resolution entirely. Consider a valid live entry `"a"` at sequence 1 and a skipped entry `"a/"+strings.Repeat("d/", 2048)+"f"` at sequence 2. Previously, the newer descendant superseded `"a"`; now Restore writes the obsolete file. If `"a"` is an existing directory, `writeInside` removes its contents first. Present also starts requiring that obsolete file. Preserve skipped descendants’ precedence over in-cap ancestors without walking their entire depth. |
| + |
| +2. **P2 — Scanning still walks an exempt skipped path’s ancestors without a bound.** At [sync.go:165](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/hostfiles-cap-live-entries/internal/hostfiles/sync.go:165), inherited skipped paths reach `skipped()` before StageDelete’s cap guard. With a non-nil callback returning false, [sync.go:50](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/hostfiles-cap-live-entries/internal/hostfiles/sync.go:50) repeatedly calls `path.Dir`, retaining the quadratic, uncancellable processing identified in round 4. This happens even on an empty restored tree; no deep directories need exist. Skip over-cap inherited entries before exclusion processing. Add a scan regression with a non-nil callback—the existing regression passes `nil`. |
| + |
| +Round 4’s deleted-plus-skipped bypass is closed. Its `superseded()` slowdown is removed, but finding 1 prevents treating that fix as complete. |
| + |
| +Other checks: validation and StageDelete still perform linear `CleanPath` processing; I found no additional ancestor walks in moves or relocation’s manifest-copy paths. The census envelope hash matches exactly; legacy units and seals remain unchanged. |
| + |
| +Static review only. No tests or builds ran, including the full `internal/host` suite. |
| + |
| +--- |
| + |
| +**FIRST PROMPT:** |
| +Review brief 5 at `c29cc49f`, read-only; avoid the full internal/host suite. |
| + |
| +**WHAT:** |
| +Check the round-4 fixes, remaining path processing, and merge readiness. |
| + |
| +**STATUS:** |
| +Review complete: two P2 findings; request changes. |
| + |
| +**NEEDED:** |
| +Nothing from you for this review. |
| + |
| +**RECOMMENDED:** |
| +Fix both findings and add focused regressions before merging. |
| + |
| +**SESSION:** |
| +Safe to close. HEAD and working tree are unchanged; no files or background processes were created. |
| \ No newline at end of file |
diff --git a/internal/hostfiles/fix4_linux_test.go b/internal/hostfiles/fix4_linux_test.go| index 35b9f0619..5924e8715 100644 |
| --- a/internal/hostfiles/fix4_linux_test.go |
| +++ b/internal/hostfiles/fix4_linux_test.go |
| @@ -77,19 +77,16 @@ func TestTheWritersRefuseAPathPastTheCapAsTheReadersDo(t *testing.T) { |
| if err := p.StageSkipped(past, "too-large"); !errors.Is(err, ErrTooDeep) { |
| t.Fatalf("StageSkipped past the cap: %v", err) |
| } |
| - // StageDelete of a path past the cap is a no-op: a tombstone there |
| - // would be one no reader accepts. |
| - if err := p.StageDelete(past); err != nil || p.Dirty() && p.man.Entries[past].Deleted { |
| - t.Fatalf("StageDelete of a path the manifest never had: %v", err) |
| - } |
| - // A manifest the publisher continued that already holds such a live |
| - // path (written before the cap) is refused at Commit, never written |
| - // again. A skipped entry is exempt (RestorableWithinCap). |
| + if err := p.StageDelete(past); !errors.Is(err, ErrTooDeep) { |
| + t.Fatalf("StageDelete past the cap: %v", err) |
| + } |
| + // A manifest the publisher continued that already holds such a path |
| + // (written before the cap) is refused at Commit, never written again. |
| s := release.NewDirStore(t.TempDir()) |
| q := open(t, s) |
| q.mu.Lock() |
| q.next++ |
| - q.man.Entries[past], q.dirty = liveEntry(q.next), true |
| + q.man.Entries[past], q.dirty = Entry{Seq: q.next, Skipped: "too-large"}, true |
| q.mu.Unlock() |
| if _, err := q.Commit(ctx); !errors.Is(err, ErrTooDeep) { |
| t.Fatalf("Commit of a manifest holding a path past the cap: %v", err) |
diff --git a/internal/hostfiles/fix5_linux_test.go b/internal/hostfiles/fix5_linux_test.go| index 1c826162e..767040fc5 100644 |
| --- a/internal/hostfiles/fix5_linux_test.go |
| +++ b/internal/hostfiles/fix5_linux_test.go |
| @@ -8,8 +8,6 @@ import ( |
| "encoding/json" |
| "errors" |
| "os" |
| - "path/filepath" |
| - "slices" |
| "strings" |
| "testing" |
| "time" |
| @@ -20,23 +18,7 @@ import ( |
| // inheritOverCap writes, under pfx, the manifest a writer from before the |
| // path cap could have left: a healthy published file beside an entry whose |
| // path is past MaxPathBytes. It returns the manifest's bytes. |
| -// liveEntry is a live entry at seq whose object this package would have |
| -// written under pfx. |
| -func liveEntry(seq int64) Entry { |
| - sha := strings.Repeat("a", 64) |
| - return Entry{Object: pfx + objectKey(seq, sha), SHA256: sha, Seq: seq, Size: 1} |
| -} |
| - |
| -// inheritOverCap writes, over a healthy manifest at pfx, one holding a live |
| -// path past the cap, as a writer before the cap could have. |
| func inheritOverCap(t *testing.T, s release.Store) []byte { |
| - t.Helper() |
| - return inheritPastTheCap(t, s, func(seq int64) map[string]Entry { |
| - return map[string]Entry{pathOfBytes(MaxPathBytes + 1): liveEntry(seq)} |
| - }) |
| -} |
| - |
| -func inheritPastTheCap(t *testing.T, s release.Store, extra func(seq int64) map[string]Entry) []byte { |
| t.Helper() |
| ctx := context.Background() |
| p := open(t, s) |
| @@ -47,9 +29,7 @@ func inheritPastTheCap(t *testing.T, s release.Store, extra func(seq int64) map[ |
| t.Fatal(err) |
| } |
| m.Seq++ |
| - for p, e := range extra(m.Seq) { |
| - m.Entries[p] = e |
| - } |
| + m.Entries[pathOfBytes(MaxPathBytes+1)] = Entry{Seq: m.Seq, Skipped: "too-large"} |
| body, err := json.Marshal(m) |
| if err != nil { |
| t.Fatal(err) |
| @@ -64,9 +44,9 @@ func inheritPastTheCap(t *testing.T, s release.Store, extra func(seq int64) map[ |
| return body |
| } |
| -// Fix 5 (Astra confirming P2): a manifest inherited with a live path past |
| -// the cap is refused by Open, terminally, and by every reader, and nothing |
| -// is committed over it. |
| +// Fix 5 (Astra confirming P2): a manifest inherited with a path past the |
| +// cap is refused by Open, terminally, and by every reader, and nothing is |
| +// committed over it. |
| func TestAnInheritedManifestPastTheCapIsRefusedByOpenAndTheReaders(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
| @@ -110,10 +90,9 @@ func TestRunStopsOnAManifestItCannotCommit(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - deep := pathOfBytes(MaxPathBytes + 1) |
| p.mu.Lock() |
| p.next++ |
| - p.man.Entries[deep] = liveEntry(p.next) |
| + p.man.Entries[pathOfBytes(MaxPathBytes+1)] = Entry{Seq: p.next, Skipped: "too-large"} |
| p.mu.Unlock() |
| write(t, dir, "healthy.txt", "changed") // the first scan stages it, and Commit is attempted |
| ctx, cancel := context.WithCancel(context.Background()) |
| @@ -137,152 +116,27 @@ func TestRunStopsOnAManifestItCannotCommit(t *testing.T) { |
| } |
| } |
| -// A skipped entry past the cap is exempt (moves' review of a8cebad, #236): |
| -// Restore never touches its path, so a manifest written before the cap |
| -// that holds one is still opened, continued, validated, copied and |
| -// restored, and a recovery that selects it is not stranded. A tombstone |
| -// past the cap is refused like a live entry (RestorableWithinCap): no |
| -// restore could remove it in bounded work, so a move's cut refuses such a |
| -// manifest before anything starts (#236, Paul's ruling of 2026-10-15). |
| -func TestASkippedEntryPastTheCapStaysReadableAndATombstoneDoesNot(t *testing.T) { |
| - ctx := context.Background() |
| - s, dir := release.NewDirStore(t.TempDir()), t.TempDir() |
| - skipped := pathOfBytes(MaxPathBytes + 1) |
| - inheritPastTheCap(t, s, func(seq int64) map[string]Entry { |
| - return map[string]Entry{skipped: {Seq: seq, Skipped: "too-large"}} |
| - }) |
| - m, _, err := Load(ctx, s, pfx) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - if err := RestorableWithinCap(m); err != nil { |
| - t.Fatalf("RestorableWithinCap: %v", err) |
| - } |
| - if err := ValidateManifest(m, []string{pfx}); err != nil { |
| - t.Fatalf("ValidateManifest: %v", err) |
| - } |
| - p, err := Open(ctx, s, pfx) |
| - if err != nil { |
| - t.Fatalf("Open: %v", err) |
| - } |
| - stage(t, p, "more.txt", "more") |
| - commit(t, p) |
| - const to = "instances/shop.test/abc/files/g9/" |
| - if _, err := CopyManifest(ctx, s, pfx, to, 0, nil); err != nil { |
| - t.Fatalf("CopyManifest: %v", err) |
| - } |
| - _, st, err := Restore(ctx, s, pfx, dir, nil) |
| - if err != nil { |
| - t.Fatalf("Restore: %v", err) |
| - } |
| - if st.Written != 2 || !slices.Equal(st.Skipped, []string{skipped}) { |
| - t.Fatalf("restore: %+v", st) |
| - } |
| - if b, err := os.ReadFile(filepath.Join(dir, "healthy.txt")); err != nil || string(b) != "fine" { |
| - t.Fatalf("healthy.txt: %q %v", b, err) |
| - } |
| - // A tombstone past the cap is refused, as a live entry is, by the |
| - // shared rule, every reader and Open, and nothing is touched. |
| - dead := pathOfBytes(MaxPathBytes + 2) |
| - s2 := release.NewDirStore(t.TempDir()) |
| - before := inheritPastTheCap(t, s2, func(seq int64) map[string]Entry { |
| - return map[string]Entry{dead: {Seq: seq, Deleted: true}} |
| - }) |
| - m2, _, err := Load(ctx, s2, pfx) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - if err := RestorableWithinCap(m2); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("RestorableWithinCap over a tombstone past the cap: %v", err) |
| - } |
| - if err := ValidateManifest(m2, []string{pfx}); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("ValidateManifest: %v", err) |
| - } |
| - if _, err := Open(ctx, s2, pfx); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("Open: %v", err) |
| - } |
| - if _, _, err := Restore(ctx, s2, pfx, t.TempDir(), nil); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("Restore: %v", err) |
| - } |
| - if after, _, err := s2.Get(ctx, ManifestKey(pfx)); err != nil || !bytes.Equal(before, after) { |
| - t.Fatalf("the inherited manifest was rewritten: %v", err) |
| - } |
| -} |
| - |
| -// Astra's review of the exemption (P2 1): a skipped entry past the cap, |
| -// inherited, has no file after a restore. The next scan's StageDelete of |
| -// it is a no-op rather than an error or a tombstone no reader accepts, so |
| -// the scan and its commit go through and the entry stays skipped. |
| -func TestAScanKeepsAnInheritedSkippedEntryPastTheCap(t *testing.T) { |
| - ctx := context.Background() |
| - s, dir := release.NewDirStore(t.TempDir()), t.TempDir() |
| - skipped := pathOfBytes(MaxPathBytes + 1) |
| - inheritPastTheCap(t, s, func(seq int64) map[string]Entry { |
| - return map[string]Entry{skipped: {Seq: seq, Skipped: "too-large"}} |
| - }) |
| - if _, _, err := Restore(ctx, s, pfx, dir, nil); err != nil { |
| - t.Fatalf("Restore: %v", err) |
| - } |
| - write(t, dir, "healthy.txt", "changed") |
| - p, err := Open(ctx, s, pfx) |
| - if err != nil { |
| - t.Fatalf("Open: %v", err) |
| - } |
| - if _, err := SyncDir(ctx, p, dir, nil); err != nil { |
| - t.Fatalf("scan after the restore: %v", err) |
| - } |
| - commit(t, p) |
| - m, _, err := Load(ctx, s, pfx) |
| - if err != nil { |
| - t.Fatal(err) |
| - } |
| - if e := m.Entries[skipped]; e.Skipped != "too-large" || e.Deleted { |
| - t.Fatalf("the skipped entry after the scan: %+v", e) |
| - } |
| - if e := m.Entries["healthy.txt"]; e.Deleted || e.SHA256 != sum([]byte("changed")) { |
| - t.Fatalf("healthy.txt after the scan: %+v", e) |
| - } |
| -} |
| - |
| -// Astra's round 4 (P2 2): an entry marked both deleted and skipped is a |
| -// tombstone to Restore, so the exemption does not cover it. |
| -func TestATombstoneMarkedSkippedPastTheCapIsRefused(t *testing.T) { |
| - m := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{ |
| - pathOfBytes(MaxPathBytes + 1): {Seq: 1, Deleted: true, Skipped: "too-large"}, |
| - }, Platform: map[string]Entry{}} |
| - if err := RestorableWithinCap(m); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("RestorableWithinCap: %v", err) |
| - } |
| - if err := ValidateManifest(m, []string{pfx}); !errors.Is(err, ErrTooDeep) { |
| - t.Fatalf("ValidateManifest: %v", err) |
| - } |
| -} |
| - |
| -// Astra's round 4 (P2 1): a skipped entry far past the cap costs Restore |
| -// and Present no walk of its ancestors (superseded skips it), so even a |
| -// path of 200,000 bytes restores at once. |
| -func TestAVeryDeepSkippedEntryCostsRestoreNoAncestorWalk(t *testing.T) { |
| - ctx := context.Background() |
| - s, dir := release.NewDirStore(t.TempDir()), t.TempDir() |
| - deep := strings.Repeat("d/", 100000) + "f" |
| - inheritPastTheCap(t, s, func(seq int64) map[string]Entry { |
| - return map[string]Entry{deep: {Seq: seq, Skipped: "too-large"}} |
| - }) |
| - start := time.Now() |
| - m, st, err := Restore(ctx, s, pfx, dir, nil) |
| - if err != nil { |
| - t.Fatalf("Restore: %v", err) |
| - } |
| - if took := time.Since(start); took > 2*time.Second { |
| - t.Fatalf("Restore took %v over a deep skipped entry", took) |
| - } |
| - if !slices.Equal(st.Skipped, []string{deep}) { |
| - t.Fatalf("restore: %+v", st) |
| - } |
| - if ok, err := Present(dir, filepath.Join(t.TempDir(), "shop.example.env"), m, true); err != nil || !ok { |
| - t.Fatalf("Present: %v %v", ok, err) |
| - } |
| - if took := time.Since(start); took > 4*time.Second { |
| - t.Fatalf("Restore and Present took %v over a deep skipped entry", took) |
| +// RestorableWithinCap is the readers' rule, exported for the checks made |
| +// before work that needs a manifest restored (#236): an entry of any kind |
| +// past the cap, live, tombstoned or skipped, is ErrTooDeep, and every |
| +// reader refuses the same manifest. |
| +func TestRestorableWithinCapIsTheReadersRule(t *testing.T) { |
| + sha := strings.Repeat("a", 64) |
| + for name, e := range map[string]Entry{ |
| + "live": {Object: pfx + objectKey(1, sha), SHA256: sha, Seq: 1, Size: 1}, |
| + "tombstone": {Seq: 1, Deleted: true}, |
| + "skipped": {Seq: 1, Skipped: "too-large"}, |
| + } { |
| + past := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{pathOfBytes(MaxPathBytes + 1): e}, Platform: map[string]Entry{}} |
| + if err := RestorableWithinCap(past); !errors.Is(err, ErrTooDeep) { |
| + t.Fatalf("%s past the cap: %v", name, err) |
| + } |
| + if err := ValidateManifest(past, []string{pfx}); !errors.Is(err, ErrTooDeep) { |
| + t.Fatalf("%s past the cap, ValidateManifest: %v", name, err) |
| + } |
| + at := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{pathOfBytes(MaxPathBytes): e}, Platform: map[string]Entry{}} |
| + if err := RestorableWithinCap(at); err != nil { |
| + t.Fatalf("%s at the cap: %v", name, err) |
| + } |
| } |
| } |
diff --git a/internal/hostfiles/publisher.go b/internal/hostfiles/publisher.go| index 366138929..19f1f80ac 100644 |
| --- a/internal/hostfiles/publisher.go |
| +++ b/internal/hostfiles/publisher.go |
| @@ -57,24 +57,19 @@ func Open(ctx context.Context, s release.Store, prefix string) (*Publisher, erro |
| return &Publisher{s: s, prefix: prefix, man: m, etag: etag, next: m.Seq}, nil |
| } |
| -// RestorableWithinCap holds every live entry and tombstone path of m to |
| -// the tree's length cap (withinTreeCap): the one rule the readers |
| -// (checkManifest), Open, OpenAt and Commit apply, and the check a caller |
| -// makes before it starts work that will need m restored (a move's cut). A |
| -// skipped entry (one not also marked deleted, which Restore would treat as |
| -// a tombstone) is exempt: Restore never touches its path, so a manifest |
| -// written before the cap that holds one past it stays readable and |
| -// continuable. A live entry or tombstone past the cap is ErrTooDeep: no |
| -// restore could recreate or remove it in bounded work. |
| +// RestorableWithinCap is the check a caller makes before it starts work |
| +// that will need m restored (a move's cut, the claim's files backfill): |
| +// every entry path of m, live, tombstoned or skipped, within the tree's |
| +// length cap. A manifest that fails it is ErrTooDeep: no reader accepts |
| +// it, so an operator replaces it before anything that needs it starts |
| +// (#236, Paul's ruling of 2026-10-15). |
| func RestorableWithinCap(m Manifest) error { return manifestWithinCap(m) } |
| -// manifestWithinCap is RestorableWithinCap. Open, OpenAt and Commit call |
| -// it by this name, which the root-read and record-write censuses pin. |
| +// manifestWithinCap holds every entry path of m to withinTreeCap, the cap |
| +// the readers apply: Open refuses to continue a manifest that fails it, |
| +// and Commit refuses to write one. |
| func manifestWithinCap(m Manifest) error { |
| - for path, e := range m.Entries { |
| - if e.Skipped != "" && !e.Deleted { |
| - continue |
| - } |
| + for path := range m.Entries { |
| if err := withinTreeCap(path); err != nil { |
| return err |
| } |
| @@ -176,18 +171,14 @@ func (p *Publisher) StageSkipped(path, why string) error { |
| } |
| // StageDelete records that path is gone. Deleting a path the manifest never |
| -// had, or already has as deleted, is a no-op. So is deleting a path past |
| -// the cap: the only such entry a continuable manifest holds is a skipped |
| -// one, written before the cap, and a tombstone for it would be one no |
| -// reader accepts (RestorableWithinCap). It stays as it is; Restore never |
| -// touches a skipped path. |
| +// had, or already has as deleted, is a no-op. |
| func (p *Publisher) StageDelete(path string) error { |
| clean, err := CleanPath(path) |
| if err != nil { |
| return err |
| } |
| - if withinTreeCap(clean) != nil { |
| - return nil |
| + if err := withinTreeCap(clean); err != nil { |
| + return err |
| } |
| p.mu.Lock() |
| defer p.mu.Unlock() |
diff --git a/internal/hostfiles/restore.go b/internal/hostfiles/restore.go| index 8a42b12b0..acf477529 100644 |
| --- a/internal/hostfiles/restore.go |
| +++ b/internal/hostfiles/restore.go |
| @@ -216,14 +216,11 @@ func restore(ctx context.Context, s release.Store, prefix string, origins []stri |
| // wins and the other is superseded; equal sequences keep the deeper one. |
| // Restore leaves a superseded entry unwritten and Present does not look |
| // for it, so what Restore produces is exactly what Present accepts |
| -// (stage B final review A1). Tombstones take no part, and nor does a path |
| -// past the cap: only a skipped entry can be one (RestorableWithinCap), and |
| -// walking its ancestors would cost work without bound. Any live entry |
| -// beneath such a path is past the cap too, and refused. |
| +// (stage B final review A1). Tombstones take no part. |
| func superseded(m Manifest) map[string]bool { |
| loser := map[string]bool{} |
| for p, e := range m.Entries { |
| - if e.Deleted || withinTreeCap(p) != nil { |
| + if e.Deleted { |
| continue |
| } |
| for a := path.Dir(p); a != "."; a = path.Dir(a) { |
| @@ -267,17 +264,13 @@ func checkManifest(m Manifest, origins []string) error { |
| return fmt.Errorf("hostfiles: platform item %q: %w", name, err) |
| } |
| } |
| - // A live entry or tombstone past the cap: no restore could recreate or |
| - // remove it in bounded work, so it is refused before anything is |
| - // touched (RestorableWithinCap; skipped entries are exempt, Restore |
| - // never touches them). |
| - if err := RestorableWithinCap(m); err != nil { |
| - return err |
| - } |
| for p, e := range m.Entries { |
| if _, err := CleanPath(p); err != nil { |
| return err |
| } |
| + if err := withinTreeCap(p); err != nil { |
| + return err // no restore could recreate it: refused before anything is touched |
| + } |
| if err := checkEntry(e, origins); err != nil { |
| return fmt.Errorf("hostfiles: %s: %w", p, err) |
| } |
diff --git a/internal/hostfiles/sync.go b/internal/hostfiles/sync.go| index 49a4962fb..65158cf8b 100644 |
| --- a/internal/hostfiles/sync.go |
| +++ b/internal/hostfiles/sync.go |
| @@ -76,13 +76,10 @@ func SyncDir(ctx context.Context, p *Publisher, dir string, skip func(rel string |
| // MaxPathBytes caps a path in a files tree, relative to its root: PATH_MAX |
| // (4096) less its terminating NUL, as moves' archive contract has it. It is |
| -// the one cap every writer of a new entry (Stage, StageSkipped, the |
| -// watcher's publishOne, a walk) applies, and that Commit and every reader |
| -// (checkManifest: Restore, CopyManifest, ValidateManifest) apply to live |
| -// entries and tombstones (RestorableWithinCap), so no manifest is written |
| -// that a reader refuses. A skipped entry past it, from a manifest written |
| -// before the cap, is exempt: Restore never touches it, and StageDelete |
| -// leaves it as it is. Every path it admits can be opened by one openat2 |
| +// the one cap every writer (Stage, StageSkipped, StageDelete, Commit, the |
| +// watcher's publishOne, a walk) and every reader (checkManifest: Restore, |
| +// CopyManifest, ValidateManifest) applies, so no manifest is written that |
| +// a reader refuses. Every path it admits can be opened by one openat2 |
| // beneath the restored root (Present). It also bounds depth (about 2048 |
| // components of one byte), and with it a walk's open directories and its |
| // path strings. |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 65e5264f3..2618e1b5a 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -735,10 +735,9 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| t.Fatal(err) |
| } |
| seal = "10e024eccd584f40b64e959a105c551302585e3e14e55104fc250dae5960898a" |
| - // Census overlay hostfiles-cap-236 (amadan #236): envelope-only, for the |
| - // hostfiles path cap exempting skipped entries (RestorableWithinCap) and |
| - // StageDelete leaving one past the cap as it is. No root-read unit |
| - // changed or was added, so the legacy seal above stands. |
| + // Census overlay hostfiles-cap-236 (amadan #236): envelope-only, for |
| + // hostfiles exporting its cap rule as RestorableWithinCap. No root-read |
| + // unit changed or was added, so the legacy seal above stands. |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv b/internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv| index af181f95f..00cbdb21b 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-hostfiles-cap-236.tsv |
| @@ -1,2 +1,2 @@ |
| -# Plan 14 census overlay hostfiles-cap-236 (amadan #236, owner region HA, basis moves' review of a8cebad and Paul's ruling of 2026-10-15; loaded after seeded-instances): the hostfiles path cap exempts skipped entries (RestorableWithinCap), because Restore never touches a skipped path, so a manifest written before the cap that holds one stays readable and a moves recovery that selects it is not stranded. Live entries and tombstones past the cap are still refused. StageDelete of a path past the cap is a no-op, so the inherited skipped entry stays. Only manifestWithinCap's body (and its exported name), checkManifest, superseded (it skips a path past the cap) and StageDelete change; Open, OpenAt and Commit are byte-identical. Envelope-only: no root-read unit changed or was added, nothing runs as root that did not before, and no registry, /data, release or claim read was added, so the legacy seal stands. The removePastCap unit that census ruling hostfiles-cap-236 (platform-1e) accepted was never merged and is withdrawn. |
| -@coverage internal/hostfiles d5e64f802e6200bcdb839a96b277100929a47bf3b3cb3dcbe87d807a74957f5b |
| +# Plan 14 census overlay hostfiles-cap-236 (amadan #236, owner region HA, basis moves' review of a8cebad and Paul's rulings of 2026-10-15; loaded after seeded-instances): hostfiles exports RestorableWithinCap, the readers' existing cap rule (manifestWithinCap) under a name the checks made before work that needs a manifest restored can call (moves' cut preflight, the claim's files backfill). No reader or writer changes. Envelope-only: no root-read unit changed or was added, nothing runs as root that did not before, and no registry, /data, release or claim read was added, so the legacy seal stands. The removePastCap unit that census ruling hostfiles-cap-236 (platform-1e) accepted was never merged and is withdrawn. |
| +@coverage internal/hostfiles 38d423afa1d4085d9591adb11b7336318188687ddf25b209a8a0041f5d66957b |