carlos / platform Public

host: the landing's probe and committed wakes establish through C8 before the start check (whole-branch fixes, item 1)

The landing's first probe and its committed wake went from
repairFilesGen straight to startCheck, so C8 never published the
admitted env file, never removed the adoption record, and could not
refuse the start. Both paths now call Files.Establish before startCheck;
without a provider every landing wake is refused by name before anything
is rendered or released, as the recovery owners refuse theirs.

The probe owners' fixture binds the real provider over its bucket and
journals the prepared manifest local writes (at the flip's stamp, before
.files-gen), so every probe test runs the real C8. Regressions through
it: the probe's establishment takes P's prepared manifest at the
journalled version, stages the minted env file and drops the adoption
record; the committed wake's copies it into the fresh generation; a
missing binding refuses the start and leaves the prepared manifest as
it was; without a provider the wake is refused by name. The
trimmed-history test shows C8 refusing the unprovable history first and
the start check unweakened behind it; the first-probe order gains C8's
one work call for the local effect; the runtime wiring test takes a
pass-through provider.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev aa46cb31f2ca3f4c6ed9091f664461790c0cdf98 parent 644cae2
3 files changed, +198 −1
  • internal/host/movesprobeowners.go +18 −0
  • internal/host/movesprobeowners_fix1_test.go +9 −0
  • internal/host/movesprobeowners_test.go +171 −1
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go
index aa61e1548..695654f5d 100644
--- a/internal/host/movesprobeowners.go
+++ b/internal/host/movesprobeowners.go
@@ -732,6 +732,12 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed
if work == nil {
return hostwrite.ErrUnavailable
}
+ // Without the C8 provider every landing wake is refused by name before
+ // anything is rendered or released (whole-branch fixes, item 1), as the
+ // recovery owners refuse theirs.
+ if m.Files == nil {
+ return claimFilesRefusal()
+ }
if v.committed {
return m.materializeCommitted(ctx, o, v, in, work)
}
@@ -793,6 +799,13 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed
if err := m.repairFilesGen(ctx, o, v, work, in.Identity); err != nil {
return err
}
+ // The admitted generation's files manifest through C8 before the start
+ // check (whole-branch fixes, item 1): the probe's takes P's prepared
+ // manifest at the journalled version, stages the env file on disk and
+ // drops the adoption record; a refusal stops the start.
+ if err := m.Files.Establish(ctx, o, in, work); err != nil {
+ return err
+ }
return m.startCheck(ctx, o, in)
}
@@ -1457,6 +1470,11 @@ func (m *movesProbeOwners) materializeCommitted(ctx context.Context, o hostclaim
if err := m.repairFilesGen(ctx, o, v, work, in.Identity); err != nil {
return err
}
+ // The fresh generation's files manifest through C8, copied from the
+ // admitted predecessor's, before the start check (item 1).
+ if err := m.Files.Establish(ctx, o, in, work); err != nil {
+ return err
+ }
return m.startCheck(ctx, o, in)
}
diff --git a/internal/host/movesprobeowners_fix1_test.go b/internal/host/movesprobeowners_fix1_test.go
index 8fa715228..4e015772a 100644
--- a/internal/host/movesprobeowners_fix1_test.go
+++ b/internal/host/movesprobeowners_fix1_test.go
@@ -12,6 +12,7 @@ import (
"github.com/carlosframework/platform/internal/genselect"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
"github.com/carlosframework/platform/internal/hostguard"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/moves"
@@ -374,6 +375,14 @@ func TestMovesProbeOwnersTrimmedHistoryIsRefused(t *testing.T) {
f.writeLocal(poOp, h.Generation, f.inc)
trimmed := f.read()
before := f.bucket.LatestID("data", f.markerKey())
+ // C8 refuses the unprovable history first (item 1: the establishment
+ // runs before the start check, and a chain it cannot prove is
+ // ErrNoEvidence); behind it the start check stands unweakened.
+ err = f.owners.Materialize(ctx, trimmed, third, newFenceWork())
+ if !errors.Is(err, hostfiles.ErrNoEvidence) {
+ t.Fatalf("a trimmed history through C8: %v", err)
+ }
+ f.owners.Files = noopFiles{}
err = f.owners.Materialize(ctx, trimmed, third, newFenceWork())
if !errors.Is(err, moves.ErrStartMarker) {
t.Fatalf("a trimmed history: %v", err)
diff --git a/internal/host/movesprobeowners_test.go b/internal/host/movesprobeowners_test.go
index dccf2ec8f..97eb4d60f 100644
--- a/internal/host/movesprobeowners_test.go
+++ b/internal/host/movesprobeowners_test.go
@@ -18,8 +18,10 @@ import (
"github.com/carlosframework/platform/internal/genselect"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfiles"
"github.com/carlosframework/platform/internal/hostguard"
"github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/instancekey"
"github.com/carlosframework/platform/internal/lease/leasetest"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/moves/archive"
@@ -135,6 +137,9 @@ func newPOFixture(t *testing.T) *poFixture {
if err := j.Commit(f.dataDir); err != nil {
t.Fatal(err)
}
+ // The prepared files manifest local wrote at the flip's stamp, before
+ // .files-gen, and its binding in the op journal (C8, Task 6).
+ f.preparedManifestJournalledAt(poOp, poOp, 18, f.files, envPath)
f.route = registry.Route{Host: poHost, Kind: "instance", App: "jam", Account: "bab", Status: "landing", HoldOp: poOp, Socket: filepath.Join(f.dataDir, poHost+".sock"), DBPath: filepath.Join(f.dataDir, poHost+".db")}
f.writeLocal(poOp, poOp, inc)
pins := mpPins()
@@ -180,6 +185,9 @@ func newPOFixture(t *testing.T) *poFixture {
},
NewID: func() string { f.seq++; return fmt.Sprintf("fresh%d", f.seq) },
Now: func() time.Time { return time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC) }}
+ // The C8 provider is the real one over the fixture's bucket, as the
+ // claim assembly binds it (whole-branch fixes, item 1).
+ f.owners.Files = f.realC8()
return f
}
@@ -337,6 +345,164 @@ func TestProbeIsAColdAdmissionIntoThePreparedGeneration(t *testing.T) {
}
}
+// realC8 is the production C8 provider over the fixture's bucket, data
+// directory and files paths, as the claim assembly binds it: the probe
+// owners' Files is this, not a fake (whole-branch fixes, item 1).
+func (f *poFixture) realC8() *movesFiles {
+ return &movesFiles{Files: casBucketStore{b: f.bucket, bucket: "data"}, Data: f.bucket, Bucket: "data", HomeRegion: "ie", Region: f.owners.Region, Box: f.owners.Box, DataDir: f.dataDir, FilesDir: f.owners.FilesDir}
+}
+
+// preparedManifestJournalled builds the prepared manifest local writes for
+// generation at the installation's stamp and journals its binding, as
+// local does (movesinstall.go).
+func (f *poFixture) preparedManifestJournalled(generation, op string, stamp uint64) preparedManifest {
+ f.t.Helper()
+ _, envPath := f.owners.FilesDir(poHost)
+ return f.preparedManifestJournalledAt(generation, op, stamp, f.files, envPath)
+}
+
+func (f *poFixture) preparedManifestJournalledAt(generation, op string, stamp uint64, dir, envPath string) preparedManifest {
+ f.t.Helper()
+ pm, err := buildPreparedManifest(f.t.Context(), f.bucket, "data", "instances/"+poHost, generation, op, stamp, dir, envPath, true, hostfiles.WakeState{V: 1, Host: poHost, Timers: []hostfiles.WakeTimer{}})
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ j, err := moves.ReadJournal(f.dataDir, poHost, generation)
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ j.Mark(landStepFilesManifest, string(mustJSON(f.t, pm)))
+ if err := j.Commit(f.dataDir); err != nil {
+ f.t.Fatal(err)
+ }
+ return pm
+}
+
+// The landing's first probe and its committed wake establish the admitted
+// generation's files manifest through the registered C8 provider before
+// the start check (whole-branch fixes, item 1): the probe's establishment
+// takes P's prepared manifest at the journalled version, stages the env
+// file on disk and drops the adoption record; the committed wake's copies
+// it into the fresh generation. A refusal from C8 stops the start, and
+// without a provider the wake is refused by name.
+func TestLandingWakesEstablishThroughC8(t *testing.T) {
+ f := newPOFixture(t)
+ ctx := t.Context()
+ j, err := moves.ReadJournal(f.dataDir, poHost, poOp)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var pm preparedManifest
+ if err := json.Unmarshal([]byte(j.Steps[landStepFilesManifest]), &pm); err != nil {
+ t.Fatal(err)
+ }
+ // The launch source minted the instance key into <host>.env before the
+ // establishment (O1'): the env file differs from the prepared item.
+ env := []byte("CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=abc\n")
+ if err := os.WriteFile(filepath.Join(f.dataDir, poHost+".env"), env, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ adoption := filepath.Join(f.dataDir, poHost+instancekey.AdoptionSuffix)
+ if err := os.WriteFile(adoption, []byte(`{"v":2}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ o := f.read()
+ in, err := f.owners.Select(ctx, f.route, o, poProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ admitted := f.admit(o, in)
+ if err := f.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: f}); err != nil {
+ t.Fatalf("the probe's materialization: %v", err)
+ }
+ prefix := movesFilesPrefix("instances/"+poHost, poOp)
+ m, found, err := hostfiles.Load(ctx, casBucketStore{b: f.bucket, bucket: "data"}, prefix)
+ if err != nil || !found {
+ t.Fatal(err, found)
+ }
+ if m.Seq != 1 || m.Platform[hostfiles.EnvItem].SHA256 != sha(env) || m.Entries["a.txt"].Seq != 0 {
+ t.Fatalf("P's manifest after the probe: %+v", m)
+ }
+ if _, has := m.Platform[instancekey.AdoptionItem]; has {
+ t.Fatal("the adoption item was carried")
+ }
+ if _, err := os.Stat(adoption); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("the adoption record remains: %v", err)
+ }
+ if raw, err := f.bucket.GetVersion(ctx, "data", pm.Key, pm.Version); err != nil || sha(raw) != pm.SHA256 {
+ t.Fatalf("the prepared version: %v", err)
+ }
+ if err := f.owners.Publish(ctx, admitted, in); err != nil {
+ t.Fatal(err)
+ }
+
+ // The committed wake: the claim at landing/committed, the record
+ // committed, a cold admission into a fresh generation whose files
+ // manifest is copied from P's.
+ c := *f.read().Claim
+ c.Op.Phase = hostclaim.PhaseLandingCommitted
+ f.putClaim(&c)
+ f.rec.Move.State, f.rec.Move.LandingPhase = provision.MoveCommitted, ""
+ f.route.Status, f.route.HoldOp = "ready", ""
+ o = f.read()
+ in, err = f.owners.Select(ctx, f.route, o, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second})
+ if err != nil {
+ t.Fatalf("the committed select: %v", err)
+ }
+ if in.Prepared || in.Admission.NewGeneration == poOp {
+ t.Fatalf("the committed admission: %+v", in.Admission)
+ }
+ admitted = f.admit(o, in)
+ f.writeLocal(poOp, admitted.Claim.Holder.Generation, f.inc)
+ if err := f.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: f}); err != nil {
+ t.Fatalf("the committed materialization: %v", err)
+ }
+ fresh, found, err := hostfiles.Load(ctx, casBucketStore{b: f.bucket, bucket: "data"}, movesFilesPrefix("instances/"+poHost, in.Admission.NewGeneration))
+ if err != nil || !found {
+ t.Fatalf("the committed wake's manifest: %v %v", err, found)
+ }
+ if fresh.Entries["a.txt"].Object != m.Entries["a.txt"].Object || fresh.Platform[hostfiles.EnvItem].SHA256 != sha(env) {
+ t.Fatalf("the committed wake's manifest: %+v", fresh)
+ }
+
+ // C8's refusal stops the start: no binding in the op journal.
+ g := newPOFixture(t)
+ gj, err := moves.ReadJournal(g.dataDir, poHost, poOp)
+ if err != nil {
+ t.Fatal(err)
+ }
+ gj.Unmark(landStepFilesManifest)
+ if err := gj.Commit(g.dataDir); err != nil {
+ t.Fatal(err)
+ }
+ o = g.read()
+ in, err = g.owners.Select(ctx, g.route, o, poProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ admitted = g.admit(o, in)
+ if err := g.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: g}); err == nil || !errors.Is(err, errLandingChanged) {
+ t.Fatalf("a probe without a prepared manifest binding: %v", err)
+ }
+ if m, found, _ := hostfiles.Load(ctx, casBucketStore{b: g.bucket, bucket: "data"}, movesFilesPrefix("instances/"+poHost, poOp)); !found || m.Seq != 0 {
+ t.Fatalf("a refused establishment changed the prepared manifest: %+v %v", m, found)
+ }
+ // Without a provider the wake is refused by name, before anything.
+ g.owners.Files = nil
+ var dep *ClaimDependencyError
+ if err := g.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: g}); !errors.As(err, &dep) {
+ t.Fatalf("no provider: %v", err)
+ }
+}
+
+// noopFiles is a C8 provider that establishes nothing: for tests of the
+// runtime's wiring, where the real provider's inputs are not laid out.
+type noopFiles struct{}
+
+func (noopFiles) Establish(ctx context.Context, _ hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
+ return work.With(ctx, in.Identity, func() error { return nil })
+}
+
func mpLaunchIdentity(p *provision.MoveInputs) hostwrite.Identity {
return hostwrite.Identity{Config: HeldConfigIdentity(p.BundleETags, p.OverlayETag).Config, Release: p.Release}
}
@@ -491,6 +657,10 @@ func TestFirstProbeFromTheSourceGenerationIsAdmitted(t *testing.T) {
// The prepared marker under the writer's fence: before the request
// and in the store before its attempt.
"spawn:" + launch.Config + "|" + launch.Release, "spawn:" + launch.Config + "|" + launch.Release,
+ // C8's establishment (item 1): the env file is still the prepared
+ // item's and there is no adoption item, so nothing is published and
+ // only the local effect runs under the work.
+ "spawn:" + launch.Config + "|" + launch.Release,
}
if !reflect.DeepEqual(f.order, want) {
t.Fatalf("order:\n got %q\nwant %q", f.order, want)
@@ -679,7 +849,7 @@ func TestMovesProbeOwnersThroughTheClaimRuntime(t *testing.T) {
records := func(context.Context, string, string, string) (*provision.Record, error) { return rec, nil }
var order []string
launches := 0
- owners := &movesProbeOwners{Claims: f.claims, Records: records, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1", DataDir: journalDir, Markers: plan9D5Markers{bucket},
+ owners := &movesProbeOwners{Claims: f.claims, Records: records, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1", DataDir: journalDir, Markers: plan9D5Markers{bucket}, Files: noopFiles{},
ReadyPath: func(context.Context, string, string) (string, error) { return "/ready", nil },
FilesDir: func(string) (string, string) { return filesDir, "" },
Route: func(string) (*registry.Route, error) { rt := route; return &rt, nil },