carlos / platform Public

hostclaim: a relocating probe with no P journaled may not certify its predecessor

relocationProbeOpening added nothing when no prepared generation was
journaled, so a cold relocating probe with PredecessorCertified was
admitted with no release, no pin and no writes-lost declaration. It is
now refused; main's uncertified cold probe is unchanged.

Merge review fix round 1, finding 6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev db50d292fbc332de17099b036573cc3afd5124d7 parent 2768377
2 files changed, +16 −1
  • internal/hostclaim/generation.go +6 −1
  • internal/hostclaim/relocation_test.go +10 −0
diff --git a/internal/hostclaim/generation.go b/internal/hostclaim/generation.go
index 3229f98bf..d0d10fa7f 100644
--- a/internal/hostclaim/generation.go
+++ b/internal/hostclaim/generation.go
@@ -308,7 +308,12 @@ func relocationProbeOpening(c *Claim, in Admission) error {
p, opened := c.Op.Journal["prepared"], preparedOpened(c)
if p == "" {
// No prepared generation journaled: main's cold probe as claim Plan
- // 11 accepted it. Region HA's ToProbing always journals P first.
+ // 11 accepted it (region HA's ToProbing always journals P first).
+ // With no P there is no release to back a certification, so a
+ // certified relocating probe is refused.
+ if in.PredecessorCertified {
+ return fmt.Errorf("%w: a relocating probe certifies only P's opening", ErrWrongState)
+ }
return nil
}
if (!opened && in.NewGeneration != p) || (opened && in.NewGeneration == p) {
diff --git a/internal/hostclaim/relocation_test.go b/internal/hostclaim/relocation_test.go
index e0f76845e..b7ae71060 100644
--- a/internal/hostclaim/relocation_test.go
+++ b/internal/hostclaim/relocation_test.go
@@ -697,6 +697,16 @@ func TestRelocationAdmissionRules(t *testing.T) {
t.Fatalf("admitted: %v", err)
}
})
+ t.Run("with no P journaled a probe certifies nothing", func(t *testing.T) {
+ s, o := relAtPhase(t, PhaseRelocatingProbing)
+ o = step(t, s, o, func(c *Claim) error { delete(c.Op.Journal, "prepared"); return nil })
+ if _, err := Update(ctx, s, o, AdmitWriter(probe(o, "P9", true))); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("a certified probe with no P was admitted: %v", err)
+ }
+ if _, err := Update(ctx, s, o, AdmitWriter(probe(o, "P9", false))); err != nil {
+ t.Fatalf("main's uncertified cold probe: %v", err)
+ }
+ })
}
func TestRelocationEmptyPinNeedsItsEvidence(t *testing.T) {