carlos / platform Public

host: a recovery's files baseline is the recorded source restore, src_gen (whole-branch fixes, item 3)

Establish walked the ordinary admission predecessor chain. A recovery
admission records the target's last generation as its predecessor, in
the other region, so the walk stopped at once; the fresh recovery
generation had no manifest, and C8 refused with ErrNoEvidence although
src_gen's manifest was valid in this bucket. The recovery database's
source pin never reached the files provider.

For a restoring or reverting op the chain now starts at the source's
generation the claim journalled at the flip (recoverySourceGeneration:
src_gen, the same pin the database restore uses), then its own
admissions (movesFilesChainFrom). The origin proof still goes only
through movesOriginProof with the bound observation: chainOriginProof
proves src_gen from the claim's journal, and a proof that refuses it
refuses the whole. A legacy source (src_gen "") names no files
generation and refuses, never a bootstrap.

Regressions: clean, declared-loss and fenced returns, and a revert, all
establish the fresh generation from src_gen's manifest with the carried
set src_gen plus the new generation; a proof refusing src_gen refuses;
a legacy source refuses with ErrNoEvidence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6bc852edd20b979c57d73c53d0f2eff46e3b96db parent 0003187
2 files changed, +128 −4
  • internal/host/movesfiles.go +46 −4
  • internal/host/movesfiles_test.go +82 −0
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go
index 521b1883e..bccf114a6 100644
--- a/internal/host/movesfiles.go
+++ b/internal/host/movesfiles.go
@@ -332,10 +332,36 @@ var readFileBounded = func(path string, limit int64) ([]byte, bool, error) {
// manifest never references a source-region object (P6-D3). Warm
// admissions keep their generation and are passed over.
func movesFilesChain(h *hostclaim.Holder, home, region string) []string {
- if h == nil || !movesFilesGeneration(h.Generation) {
+ if h == nil {
return nil
}
- cur := h.Generation
+ return movesFilesChainFrom(h, h.Generation, home, region)
+}
+
+// recoverySourceGeneration is the generation a recovery's files baseline
+// starts from (whole-branch fixes, item 3): for a restoring or reverting
+// op, the source's generation the claim journalled at the flip (src_gen),
+// which the recovery's database restore is pinned to as well; "" names a
+// legacy source with no files generation. ok is false for any other op.
+func recoverySourceGeneration(c *hostclaim.Claim) (string, bool) {
+ if c == nil {
+ return "", false
+ }
+ switch c.Op.Kind {
+ case hostclaim.KindRestoring, hostclaim.KindReverting:
+ return c.Op.Journal[hostclaim.JournalSrcGen], true
+ }
+ return "", false
+}
+
+// movesFilesChainFrom is movesFilesChain started at generation start
+// instead of the holder's: start, then each cold admission's predecessor
+// in this region, as the admissions record them.
+func movesFilesChainFrom(h *hostclaim.Holder, start, home, region string) []string {
+ if h == nil || !movesFilesGeneration(start) {
+ return nil
+ }
+ cur := start
chain := []string{cur}
seen := map[string]bool{cur: true}
for i := len(h.Admissions) - 1; i >= 0; i-- {
@@ -438,7 +464,16 @@ func (p chainOriginProof) Proven(_ context.Context, o hostclaim.Observed, _ host
if generation == "" || o.Absent || o.Claim == nil || o.Claim.Holder == nil {
return false, nil
}
- return slices.Contains(movesFilesChain(o.Claim.Holder, p.home, p.region), generation), nil
+ if slices.Contains(movesFilesChain(o.Claim.Holder, p.home, p.region), generation) {
+ return true, nil
+ }
+ // A recovery's source generation (item 3): the claim journalled it at
+ // the flip, and the recovery restores from it; the chain through its
+ // own admissions follows.
+ if src, ok := recoverySourceGeneration(o.Claim); ok && src != "" {
+ return slices.Contains(movesFilesChainFrom(o.Claim.Holder, src, p.home, p.region), generation), nil
+ }
+ return false, nil
}
func (f *movesFiles) proof() movesOriginProof {
@@ -857,7 +892,14 @@ func (f *movesFiles) Establish(ctx context.Context, o hostclaim.Observed, in Cla
}
p.SetOrigins([]string{own})
default:
- base, origins, found, err := f.newestManifest(ctx, o, in.Admission, h.Location, movesFilesChain(h, f.HomeRegion, f.Region))
+ // The chain: the holder's, or for a recovery the recorded source
+ // restore's (src_gen; item 3), since the recovery admission's
+ // predecessor is the target's generation in the other region.
+ chain := movesFilesChain(h, f.HomeRegion, f.Region)
+ if src, ok := recoverySourceGeneration(o.Claim); ok {
+ chain = movesFilesChainFrom(h, src, f.HomeRegion, f.Region)
+ }
+ base, origins, found, err := f.newestManifest(ctx, o, in.Admission, h.Location, chain)
if err != nil {
return err
}
diff --git a/internal/host/movesfiles_test.go b/internal/host/movesfiles_test.go
index f86dccdac..ee3af098d 100644
--- a/internal/host/movesfiles_test.go
+++ b/internal/host/movesfiles_test.go
@@ -1131,6 +1131,88 @@ func TestEstablishProvesAndCarriesOnlyReferencedOrigins(t *testing.T) {
}
}
+// recoveryObserved is the source box's observation after a recovery
+// admission: the holder at the fresh recovery generation R, admitted after
+// the target's admission T in the other region, under a restoring op (or
+// a reverting one) whose journal names the source's generation src_gen,
+// as the flip wrote it.
+func recoveryObserved(kind, srcGen, target, recovered string) hostclaim.Observed {
+ o := servingObserved(recovered, "src:uk:@root:uk", target+":ie:src:uk", recovered+":uk:"+target+":ie")
+ if srcGen == "" {
+ o.Claim.Holder.Admissions = o.Claim.Holder.Admissions[1:] // a legacy source: no admission of its own
+ }
+ o.Claim.Op = hostclaim.Op{Kind: kind, ID: target, Phase: hostclaim.PhaseProbing, Exec: "uk-box", Participants: []string{"uk-box", "ie-box"},
+ Intent: map[string]string{"to": "ie", "source": "uk-box", "target": "ie-box", "rollback": "rb1"},
+ Journal: map[string]string{hostclaim.JournalSrcGen: srcGen, hostclaim.JournalSourceStamp: "15", hostclaim.JournalCutSession: "3"}}
+ return o
+}
+
+// A recovery admission's files baseline is the recorded source restore
+// (whole-branch fixes, item 3): for a restoring or reverting op the chain
+// walk starts at the journalled src_gen, pinned as the database's source
+// restore is, not at the admission's predecessor, which is the target's
+// generation in the other region and ends the ordinary walk at once. The
+// origin proof still goes only through movesOriginProof with the bound
+// observation: src_gen is proven by the claim's journal, and a proof that
+// refuses it refuses the whole. Clean, declared-loss and fenced returns,
+// and a revert, all establish from src_gen's manifest; a legacy source
+// (src_gen "") has no files generation and refuses.
+func TestRecoveryEstablishesFromTheRecordedSourceGeneration(t *testing.T) {
+ ctx := context.Background()
+ for name, shape := range map[string]func(o *hostclaim.Observed){
+ "a clean return": func(*hostclaim.Observed) {},
+ "a declared-loss return": func(o *hostclaim.Observed) {
+ o.Claim.Op.Journal["loss.l1"] = "1"
+ o.Claim.LossRefs = []string{"control/loss-records/l1.json"}
+ },
+ "a fenced return": func(o *hostclaim.Observed) {
+ o.Claim.Op.Journal["fence.ie-box"] = `{"enrollment":"enr","key":"control/box-fences/enr.json","sha256":"ab"}`
+ },
+ "a revert": func(o *hostclaim.Observed) {
+ o.Claim.Op.Kind = hostclaim.KindReverting
+ delete(o.Claim.Op.Intent, "rollback")
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ src := x.publish("src", func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ })
+ o := recoveryObserved(hostclaim.KindRestoring, "src", "mv1", "r1")
+ shape(&o)
+ if err := x.f.Establish(ctx, o, launchInputs("r1"), x); err != nil {
+ t.Fatalf("establish from src_gen: %v", err)
+ }
+ m, found := x.manifest("r1")
+ if !found || m.Entries["a.txt"].Object != src.Entries["a.txt"].Object || !reflect.DeepEqual(m.Origins, []string{movesFilesPrefix(filesLoc, "r1"), movesFilesPrefix(filesLoc, "src")}) {
+ t.Fatalf("r1: %+v %v", m, found)
+ }
+ // The proof has the final say over src_gen.
+ y := newFilesFixture(t)
+ y.writeEnv("K=1\n")
+ y.publish("src", func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ })
+ y.f.Proof = answeringProof{proven: map[string]bool{"r1": true}}
+ if err := y.f.Establish(ctx, o, launchInputs("r1"), y); !errors.Is(err, hostfiles.ErrOrigin) {
+ t.Fatalf("src_gen the proof refuses: %v", err)
+ }
+ })
+ }
+ // A legacy source: src_gen "" names no files generation; nothing to
+ // establish from, and never a bootstrap.
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ if err := x.f.Establish(ctx, recoveryObserved(hostclaim.KindRestoring, "", "mv1", "r1"), launchInputs("r1"), x); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("a legacy source: %v", err)
+ }
+}
+
// The instance key rule (O1'): the manifest Establish leaves carries the
// env file with exactly the bytes on disk when it runs, staged and
// committed before it returns, and no adoption item; the local adoption