carlos / platform Public

plan: claim Plan 8b rev 5, legacy updates need both floor objects absent (review round 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b399eaf8131c99f8a1984616d7ee9ebaac074110 parent 36620d3
1 file changed, +28 −4
  • docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md +28 −4

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md
index fb44ac457..be20b2aed 100644
--- a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md
+++ b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md
@@ -1,6 +1,6 @@
# Host lifecycle claim, Plan 8b: the floor, the roster and live fencing
-Rev 4 answers review round 3 (three P2).
+Rev 4 answers review round 3 (three P2). Rev 5 (controller) requires both the floor and its authorization to be absent before the legacy update path (review round 4); the Task 5 test step must cover both incomplete pairs (floor without authorization, authorization without floor), each refusing with hostfloor.ErrFloor.
Implementation plan, 2026-10-06. Branch to implement: `feat/hostclaim-floor`,
from `feat/hostclaim-leases` at
@@ -5957,6 +5957,26 @@ func activationFixture(t *testing.T) (*Server, hostactors.Directory, release.Sys
return s, hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"}, next
}
+// Legacy promotion needs both floor objects absent; either half alone is
+// an unfinished publication (review round 4).
+func TestHostUpdateReservationRefusesIncompleteFloorPair(t *testing.T) {
+ for _, drop := range []string{hostfloor.Key, hostfloor.AuthKey} {
+ t.Run(drop, func(t *testing.T) {
+ s, _, target := activationFixture(t)
+ etag := ""
+ if _, e, err := s.cfg.Releases.Get(t.Context(), drop); err == nil {
+ etag = e
+ }
+ if err := s.cfg.Releases.Delete(t.Context(), drop, etag); err != nil {
+ t.Skipf("store cannot delete %s in this fixture: %v", drop, err)
+ }
+ if id, err := s.reserveHostUpdate(t.Context(), &target, "stable", "promote", release.SystemPromoteOptions{}); !errors.Is(err, hostfloor.ErrFloor) || id != "" {
+ t.Fatalf("incomplete pair without %s = %q, %v; want ErrFloor", drop, id, err)
+ }
+ })
+ }
+}
+
func TestHostUpdateReservationRetriesPublication(t *testing.T) {
for _, fault := range []string{"crash", "failed", "committed", "unreadable"} {
t.Run(fault, func(t *testing.T) {
@@ -7141,9 +7161,13 @@ func (s *Server) reserveHostUpdate(ctx context.Context, rel *release.SystemRelea
pub := release.RootSigner(s.cfg.Key).RootPublicKey()
floor, err := hostfloor.Read(ctx, s.cfg.Releases, pub, s.cfg.Bucket)
if errors.Is(err, os.ErrNotExist) {
- // An incomplete publication is not the old deployment mode.
- if _, _, e := s.cfg.Releases.Get(ctx, hostfloor.AuthKey); !errors.Is(e, os.ErrNotExist) {
- return "", hostfloor.ErrFloor
+ // Legacy mode only when BOTH the floor and its authorization are
+ // absent; any incomplete pair is an unfinished publication, never
+ // the old deployment mode (review round 4).
+ for _, key := range []string{hostfloor.Key, hostfloor.AuthKey} {
+ if _, _, e := s.cfg.Releases.Get(ctx, key); !errors.Is(e, os.ErrNotExist) {
+ return "", hostfloor.ErrFloor
+ }
}
return "", nil
}