carlos / platform Public

host: src_gen only bridges a recovery with no manifest of its own; later admissions follow their lineage (whole-branch fixes, item 3 re-ruled; platform-1e P1)

Every non-prepared restoring or reverting wake replaced the chain with
the src_gen chain, so newer recovery manifests were ignored: a dirty
return's second cold admission published the stopped source's a.txt=A
over the rollback manifest's B (or ErrNoEvidence with the source
manifest absent), and a clean retry after publishing its own root took
the source's again.

The holder's own chain now comes first and the newest authorized
manifest through it wins (the prepared rollback generation's, a later
recovery publication's, a revert's own lineage), through the normal
chain and origin proof. src_gen bridges only a recovery that restores
the recorded source baseline (restoresSourceBaseline: a restoring op
with no return seal journalled, so never a dirty return) and only while
its lineage holds no manifest, mirroring the database selector's
not-rooted case.

Real-C8 restart regressions: a restart after the prepared rollback
manifest publishes B, with the source present or absent; a dirty
return's second and third admissions follow the rollback generation and
its certified descendant; a dirty return with no manifest in its
lineage refuses rather than bridging; a clean return and a revert after
a newer recovery publication take the newer one; a first admission with
no recovery manifest still bridges from src_gen. The no-mint rule is
pinned: a recovery probe (dirty and clean) through the real C8 keeps
the restored env byte for byte, mints no CARLOS_INSTANCE_KEY, and the
generation's env item is that env.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 75c31df61772fe3378f3e36f8969cbedc6a8258c parent a5347c2
3 files changed, +220 −8
  • internal/host/movesfiles.go +33 −8
  • internal/host/movesfiles_test.go +130 −0
  • internal/host/movesrecoveryowners_test.go +57 −0
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go
index bccf114a6..d694da7b3 100644
--- a/internal/host/movesfiles.go
+++ b/internal/host/movesfiles.go
@@ -354,6 +354,22 @@ func recoverySourceGeneration(c *hostclaim.Claim) (string, bool) {
return "", false
}
+// restoresSourceBaseline reports whether a recovery admission restores the
+// recorded source baseline (platform-1e's framing of item 3): a restoring
+// op that is not a dirty return (a dirty return journals its return seal
+// and restores the rollback generation or a certified descendant, never
+// the stopped source), with a journalled src_gen. The files bridge from
+// src_gen applies only to these, and only while their recovery lineage
+// holds no manifest (the database selector's "not rooted"); every other
+// admission follows its recovery lineage.
+func restoresSourceBaseline(c *hostclaim.Claim) bool {
+ if c == nil || c.Op.Kind != hostclaim.KindRestoring || c.Op.Journal[moves.JournalReturnSeal] != "" {
+ return false
+ }
+ src, ok := recoverySourceGeneration(c)
+ return ok && src != ""
+}
+
// 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.
@@ -892,17 +908,26 @@ func (f *movesFiles) Establish(ctx context.Context, o hostclaim.Observed, in Cla
}
p.SetOrigins([]string{own})
default:
- // 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)
+ // The chain is the holder's: the newest authorized manifest through
+ // it wins, a recovery's own included (the prepared rollback
+ // generation's, or a later recovery publication's). Only when that
+ // chain holds no manifest, the first recovery admission, whose
+ // predecessor is the target's generation in the other region, does
+ // src_gen bridge the missing source baseline (item 3 as re-ruled;
+ // the database selector's distinction, movesrecoveryowners.go
+ // baseline: the recorded source restore only while the recovery has
+ // no root of its own).
+ base, origins, found, err := f.newestManifest(ctx, o, in.Admission, h.Location, movesFilesChain(h, f.HomeRegion, f.Region))
if err != nil {
return err
}
+ if !found && restoresSourceBaseline(o.Claim) {
+ src, _ := recoverySourceGeneration(o.Claim)
+ base, origins, found, err = f.newestManifest(ctx, o, in.Admission, h.Location, movesFilesChainFrom(h, src, f.HomeRegion, f.Region))
+ if err != nil {
+ return err
+ }
+ }
if !found {
never := false
if f.Evidence != nil {
diff --git a/internal/host/movesfiles_test.go b/internal/host/movesfiles_test.go
index ee3af098d..1d464a9e1 100644
--- a/internal/host/movesfiles_test.go
+++ b/internal/host/movesfiles_test.go
@@ -1169,8 +1169,13 @@ func TestRecoveryEstablishesFromTheRecordedSourceGeneration(t *testing.T) {
o.Claim.Op.Journal["fence.ie-box"] = `{"enrollment":"enr","key":"control/box-fences/enr.json","sha256":"ab"}`
},
"a revert": func(o *hostclaim.Observed) {
+ // A revert's holder never left the source: its admission's
+ // predecessor is src in this region, so it follows its own
+ // lineage with no bridge (platform-1e's framing).
o.Claim.Op.Kind = hostclaim.KindReverting
delete(o.Claim.Op.Intent, "rollback")
+ o.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "src", Region: "uk", Predecessor: "@root", PredecessorRegion: "uk", Mode: "cold", Session: 3, Box: "uk-box"},
+ {Generation: "r1", Region: "uk", Predecessor: "src", PredecessorRegion: "uk", Mode: "cold", Session: 3, Box: "uk-box"}}
},
} {
t.Run(name, func(t *testing.T) {
@@ -1213,6 +1218,131 @@ func TestRecoveryEstablishesFromTheRecordedSourceGeneration(t *testing.T) {
}
}
+// recoveryRestartObserved is the source box's observation after a later
+// recovery admission: the holder at the fresh generation `next`, admitted
+// after the recovery generation `prev` in this region (the prepared
+// rollback generation, or an earlier recovery admission), itself admitted
+// after the target's T in the other region.
+func recoveryRestartObserved(kind, prev, next string) hostclaim.Observed {
+ o := servingObserved(next, "src:uk:@root:uk", "mv1:ie:src:uk", prev+":uk:mv1:ie", next+":uk:"+prev+":uk")
+ o.Claim.Op = hostclaim.Op{Kind: kind, ID: "mv1", 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: "src", hostclaim.JournalSourceStamp: "15", hostclaim.JournalCutSession: "3"}}
+ if kind == hostclaim.KindReverting {
+ delete(o.Claim.Op.Intent, "rollback")
+ }
+ return o
+}
+
+// src_gen only bridges the missing source baseline (the re-review's
+// ruling on item 3, mirroring the database selector): once a recovery
+// manifest exists, the prepared rollback generation's or a later recovery
+// publication's, the newest authorized recovery manifest wins through the
+// normal chain and origin proof. A restart after the dirty return's
+// prepared rollback manifest publishes the rollback's contents (B), not
+// the source's (A), even with the source manifest absent; a clean return
+// and a revert after a newer recovery publication take the newer one; a
+// first admission with no recovery manifest still bridges from src_gen.
+func TestRecoveryRestartTakesTheNewestRecoveryManifest(t *testing.T) {
+ ctx := context.Background()
+ publish := func(x *filesFixture, gen, body string) hostfiles.Manifest {
+ return x.publish(gen, func(p *hostfiles.Publisher) {
+ if err := p.Stage(ctx, "a.txt", []byte(body), 0o600, false); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+ t.Run("a restart after the prepared rollback manifest publishes B", func(t *testing.T) {
+ for name, withSource := range map[string]bool{"source present": true, "source absent": false} {
+ t.Run(name, func(t *testing.T) {
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ if withSource {
+ publish(x, "src", "A")
+ }
+ rb := publish(x, "rb1", "B")
+ o := recoveryRestartObserved(hostclaim.KindRestoring, "rb1", "r2")
+ if err := x.f.Establish(ctx, o, launchInputs("r2"), x); err != nil {
+ t.Fatalf("the restart: %v", err)
+ }
+ m, found := x.manifest("r2")
+ if !found || m.Entries["a.txt"].Object != rb.Entries["a.txt"].Object {
+ t.Fatalf("r2 took %+v, want the rollback's B (%s)", m.Entries["a.txt"], rb.Entries["a.txt"].Object)
+ }
+ })
+ }
+ })
+ for name, kind := range map[string]string{"a clean return": hostclaim.KindRestoring, "a revert": hostclaim.KindReverting} {
+ t.Run(name+" after a newer recovery publication", func(t *testing.T) {
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ publish(x, "src", "A")
+ newer := publish(x, "r1", "C") // the earlier recovery admission's publication
+ o := recoveryRestartObserved(kind, "r1", "r2")
+ if err := x.f.Establish(ctx, o, launchInputs("r2"), x); err != nil {
+ t.Fatalf("the restart: %v", err)
+ }
+ m, found := x.manifest("r2")
+ if !found || m.Entries["a.txt"].Object != newer.Entries["a.txt"].Object {
+ t.Fatalf("r2 took %+v, want the newer recovery's C", m.Entries["a.txt"])
+ }
+ })
+ }
+ t.Run("a dirty return's later admissions follow the rollback lineage", func(t *testing.T) {
+ // The second cold admission restores from the rollback generation,
+ // the third from its certified descendant (the second's publication).
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ publish(x, "src", "A")
+ rb := publish(x, "rb1", "B")
+ o := recoveryRestartObserved(hostclaim.KindRestoring, "rb1", "r2")
+ o.Claim.Op.Journal[moves.JournalReturnSeal] = "sealed"
+ if err := x.f.Establish(ctx, o, launchInputs("r2"), x); err != nil {
+ t.Fatalf("the second admission: %v", err)
+ }
+ if m, _ := x.manifest("r2"); m.Entries["a.txt"].Object != rb.Entries["a.txt"].Object {
+ t.Fatalf("r2 took %+v, want the rollback's B", m.Entries["a.txt"])
+ }
+ r2 := publish(x, "r2", "D") // the second admission's own publication
+ o3 := servingObserved("r3", "src:uk:@root:uk", "mv1:ie:src:uk", "rb1:uk:mv1:ie", "r2:uk:rb1:uk", "r3:uk:r2:uk")
+ o3.Claim.Op = o.Claim.Op
+ if err := x.f.Establish(ctx, o3, launchInputs("r3"), x); err != nil {
+ t.Fatalf("the third admission: %v", err)
+ }
+ if m, _ := x.manifest("r3"); m.Entries["a.txt"].Object != r2.Entries["a.txt"].Object {
+ t.Fatalf("r3 took %+v, want the descendant's D", m.Entries["a.txt"])
+ }
+ })
+ t.Run("a dirty return never bridges to the source", func(t *testing.T) {
+ // A dirty return restores the rollback generation, never the stopped
+ // source: with no manifest in its lineage it refuses rather than
+ // publishing the source's older files.
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ publish(x, "src", "A")
+ o := recoveryRestartObserved(hostclaim.KindRestoring, "rb1", "r2")
+ o.Claim.Op.Journal[moves.JournalReturnSeal] = "sealed"
+ if err := x.f.Establish(ctx, o, launchInputs("r2"), x); !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("a dirty return without its rollback manifest: %v", err)
+ }
+ if _, found := x.manifest("r2"); found {
+ t.Fatal("a dirty return published the source's files")
+ }
+ })
+ t.Run("a first admission with no recovery manifest bridges from src_gen", func(t *testing.T) {
+ x := newFilesFixture(t)
+ x.writeEnv("K=1\n")
+ src := publish(x, "src", "A")
+ o := recoveryObserved(hostclaim.KindRestoring, "src", "mv1", "r1")
+ if err := x.f.Establish(ctx, o, launchInputs("r1"), x); err != nil {
+ t.Fatalf("the first admission: %v", err)
+ }
+ if m, found := x.manifest("r1"); !found || m.Entries["a.txt"].Object != src.Entries["a.txt"].Object {
+ t.Fatalf("r1 took %+v, want the source's A", m.Entries["a.txt"])
+ }
+ })
+}
+
// 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
diff --git a/internal/host/movesrecoveryowners_test.go b/internal/host/movesrecoveryowners_test.go
index 08bea426e..004ff67e2 100644
--- a/internal/host/movesrecoveryowners_test.go
+++ b/internal/host/movesrecoveryowners_test.go
@@ -373,6 +373,63 @@ func TestDirtyReturnEstablishesThePreparedManifestThroughC8(t *testing.T) {
}
}
+// The no-mint rule (platform-1e, whole-branch fixes): a recovery probe
+// preserves the restored environment byte for byte and never mints a
+// CARLOS_INSTANCE_KEY; the real C8 then carries exactly those bytes as
+// the generation's env item. A later change that minted during recovery
+// fails here, loudly, rather than exempting <host>.env from verification.
+func TestRecoveryProbeNeverMintsAnInstanceKey(t *testing.T) {
+ for _, kind := range []string{rcDirty, rcClean} {
+ t.Run(kind, func(t *testing.T) {
+ f := newRCFixture(t, kind)
+ ctx := t.Context()
+ f.owners.Files = f.realC8()
+ envPath := filepath.Join(f.dataDir, poHost+".env")
+ restored, err := os.ReadFile(envPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(restored), activator.InstanceKeyVar+"=") {
+ t.Fatalf("the restored env already carries a key: %q", restored)
+ }
+ gen := "src"
+ if kind == rcDirty {
+ gen = rpRB
+ }
+ // The manifest the recovery establishes from: the source's, or the
+ // dirty return's prepared one, built from the restored tree.
+ pm := f.preparedManifestJournalled(gen, poOp, rcS)
+ o := f.read()
+ in := f.selectAdmission(o)
+ admitted := f.admitThroughWriter(o, in)
+ if kind == rcClean {
+ // The restore placed the admitted generation's database (the
+ // start check's, not C8's).
+ if err := hostbox.WriteGenerationMarker(f.route.DBPath+".gen", admitted.Claim.Holder.Generation); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := f.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: f.poFixture}); err != nil {
+ t.Fatalf("the recovery's materialization: %v", err)
+ }
+ after, err := os.ReadFile(envPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(after) != string(restored) {
+ t.Fatalf("the recovery changed the restored env:\n%q\n%q", restored, after)
+ }
+ if strings.Contains(string(after), activator.InstanceKeyVar+"=") {
+ t.Fatalf("the recovery minted a key: %q", after)
+ }
+ m, found, err := hostfiles.Load(ctx, casBucketStore{b: f.bucket, bucket: "data"}, movesFilesPrefix("instances/"+poHost, admitted.Claim.Holder.Generation))
+ if err != nil || !found || m.Platform[hostfiles.EnvItem].SHA256 != sha(restored) {
+ t.Fatalf("the env item is not the restored env: %+v %v %v (prepared %s)", m.Platform[hostfiles.EnvItem], found, err, pm.Key)
+ }
+ })
+ }
+}
+
// Plan 4 Task 9: a clean return's probe is a cold admission into a fresh
// generation restored from src_gen in this region, its coverage
// clean-return: the admission records no writes lost.