region HA 2a: the move door refuses a destination with no room
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+346
−3
internal/console/apiinstancemove.go+15 −0internal/console/boxselect.go+33 −0internal/console/boxselect_ha_test.go+292 −0internal/provision/plan14_root_reads_test.go+1 −1internal/provision/plan9_b8_census_test.go+1 −1internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv+4 −1
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go| index 686bdb2f8..841c8f1aa 100644 |
| --- a/internal/console/apiinstancemove.go |
| +++ b/internal/console/apiinstancemove.go |
| @@ -654,6 +654,14 @@ func (s *Server) handleAPIInstanceMoveRequest(w http.ResponseWriter, r *http.Req |
| return |
| } |
| } |
| + // Region HA (spec 2 §7, H27): a destination with no box that has room |
| + // is the no-room refusal, before the placement checks below answer it |
| + // as a 400. It only refuses: nothing is chosen. An incomplete read is |
| + // the temporarily unavailable 503, never no room. |
| + if err := s.roomIn(ctx, to, ""); err != nil { |
| + writeClaimResponseError(w, err) |
| + return |
| + } |
| caps, homeNamed, published := movePlacements(ctx, s.cfg.Releases, s.moveHomeName()) |
| // Review Focus 5: a box that sets CARLOS_REGION to the home's name is |
| // home to the console but not to itself, so it cannot take a move into |
| @@ -676,6 +684,13 @@ func (s *Server) handleAPIInstanceMoveRequest(w http.ResponseWriter, r *http.Req |
| } |
| from := s.homeRegionOut(rec.Region) |
| sel, err := s.selectMoveBoxes(ctx, host, to, rec.Region, acct.Sqid+"/"+app) |
| + // No box in the destination has room (region HA spec 2 §7, H27), or the |
| + // read that would tell was incomplete: the claim doors' answers. |
| + var full *noCapacityError |
| + if errors.As(err, &full) || errors.Is(err, hostops.ErrEvidenceIncomplete) { |
| + writeClaimResponseError(w, err) |
| + return |
| + } |
| if errors.Is(err, errEdgesIncomplete) { |
| jsonError(w, http.StatusConflict, "the fleet's edges files could not all be read. try again") |
| return |
diff --git a/internal/console/boxselect.go b/internal/console/boxselect.go| index d25bed47e..79a1808bd 100644 |
| --- a/internal/console/boxselect.go |
| +++ b/internal/console/boxselect.go |
| @@ -6,8 +6,10 @@ import ( |
| "fmt" |
| "log" |
| "os" |
| + "slices" |
| "github.com/carlosframework/platform/internal/boxid" |
| + "github.com/carlosframework/platform/internal/hostops" |
| "github.com/carlosframework/platform/internal/moves" |
| ) |
| @@ -115,6 +117,11 @@ func (s *Server) selectMoveBoxes(ctx context.Context, host, to, from, app string |
| fp := s.expectedSecretsFP() |
| for _, code := range []string{to, from} { |
| cands := eligibleBoxes(snap, roster, code, aws[code], home, fp, nil) |
| + if code == to { |
| + if err := s.moveRoomCheck(ctx, to, cands); err != nil { |
| + return moveSelection{}, err |
| + } |
| + } |
| if len(cands) > 1 { |
| sel.Many, sel.ManyRegion = true, s.homeRegionOut(code) |
| return sel, nil |
| @@ -164,3 +171,29 @@ func (s *Server) verifiedRoster(ctx context.Context, snap edgesSnapshot, aws map |
| } |
| return out, nil |
| } |
| + |
| +// moveRoomCheck is a move's destination under region HA (spec 2 §7, |
| +// H27): the destination is a door that chooses a box, so when none of its |
| +// candidates is a box the create door's chooser would place on (roles, |
| +// headroom, fresh edges files), the move is the no-room refusal. It only |
| +// refuses (census ruling region-ha-2a rev 2): the candidates, and so the |
| +// destination main's selection picks, are never narrowed or changed. The |
| +// source is never checked, so an instance on an excluded box can still |
| +// move out (A10-3). With region HA off, or no claim runtime, it answers nil |
| +// and no roster is read. |
| +func (s *Server) moveRoomCheck(ctx context.Context, to string, cands []boxCandidate) error { |
| + x := s.placementChooser("") |
| + if x == nil { |
| + return nil |
| + } |
| + boxes, err := x.PlaceableBoxes(ctx, s.cfg.claimPlacementRegion(to)) |
| + if err != nil && !errors.Is(err, hostops.ErrContract) { |
| + return err |
| + } |
| + for _, c := range cands { |
| + if slices.Contains(boxes, c.BoxID) { |
| + return nil |
| + } |
| + } |
| + return s.noRoom(ctx, to) |
| +} |
diff --git a/internal/console/boxselect_ha_test.go b/internal/console/boxselect_ha_test.go| new file mode 100644 |
| index 000000000..9a53e52fb |
| --- /dev/null |
| +++ b/internal/console/boxselect_ha_test.go |
| @@ -0,0 +1,292 @@ |
| +package console |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "net/http" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// roomyEdges gives each box's edges file fresh, roomy headroom, then edit |
| +// (when set) changes uk-box's. |
| +func roomyEdges(t *testing.T, fx *consoleFixture, edit func(map[string]any), boxes ...string) { |
| + t.Helper() |
| + ctx := context.Background() |
| + for _, box := range boxes { |
| + key := edgesFilePrefix + box + ".json" |
| + raw, etag, err := fx.rel.Get(ctx, key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var m map[string]any |
| + if err := json.Unmarshal(raw, &m); err != nil { |
| + t.Fatal(err) |
| + } |
| + m["headroom"] = map[string]any{"mem_free_mib": 4000, "mem_total_mib": 8000, "disk_free_mib": 50000, "disk_total_mib": 100000, "awake": 0} |
| + m["generation"] = time.Now().UTC().Format(time.RFC3339) |
| + if box == "uk-box" && edit != nil { |
| + edit(m) |
| + } |
| + body, _ := json.Marshal(m) |
| + if _, err := fx.rel.Put(ctx, key, body, etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| +} |
| + |
| +// haRoster signs a roster naming both regions, every box of fleet, and |
| +// each box's role from roles. |
| +func haRoster(t *testing.T, fx *consoleFixture, roles map[string]string, fleet ...fleetBox) { |
| + t.Helper() |
| + boxes := map[string]hostactors.Box{} |
| + for _, b := range fleet { |
| + rb := moveRosterBox(b) |
| + rb.Role = roles[b.Box] |
| + boxes[b.Box] = rb |
| + } |
| + putMoveRoster(t, fx, hostactors.Roster{Boxes: boxes, Regions: map[string]hostactors.Region{ |
| + "ie": {Account: moveTestAccount, AWSRegion: "eu-west-1"}, |
| + "uk": {Account: moveTestAccount, AWSRegion: "eu-west-2"}, |
| + }}) |
| +} |
| + |
| +// bindRegionHA turns region HA on for fx's claim runtime, with the region |
| +// HA chooser bound as the create door binds it. |
| +func bindRegionHA(fx *consoleFixture) { |
| + fx.srv.cfg.RegionHA = true |
| + a := fx.srv.cfg.ClaimAuthority |
| + a.Namespace = "platform" |
| + a.Executors = fx.srv.executorChooser(a.Directory) |
| +} |
| + |
| +// haMoveFleet is armMoveFleet's ie-box and uk-box (and any more boxes), |
| +// each with fresh, roomy headroom, a signed roster naming both regions |
| +// with each box's role from roles, and, with ha, a region HA chooser. |
| +func haMoveFleet(t *testing.T, ha bool, roles map[string]string, more ...fleetBox) *consoleFixture { |
| + t.Helper() |
| + fx := newConsoleFixture(t) |
| + fleet := append([]fleetBox{ieBox("ie-box"), ukBox("uk-box")}, more...) |
| + armMoveFleet(t, fx, fleet...) |
| + names := []string{} |
| + for _, b := range fleet { |
| + names = append(names, b.Box) |
| + } |
| + roomyEdges(t, fx, nil, names...) |
| + haRoster(t, fx, roles, fleet...) |
| + fx.srv.cfg.HostClaim = true |
| + if ha { |
| + d := hostactors.Directory{Store: fx.rel, PublicKey: fx.pub, Bucket: moveTestBucket} |
| + fx.srv.cfg.ClaimAuthority = &hostops.ProductionAuthority{Directory: d} |
| + bindRegionHA(fx) |
| + } |
| + return fx |
| +} |
| + |
| +// Region HA spec 2 §7 (H27), census ruling region-ha-2a rev 2 item 4: the |
| +// move door's room check only refuses. With room, the selection is the |
| +// one region HA off makes; with none, it is the no-room refusal with its |
| +// alert; and it never narrows the candidates to pick a destination main's |
| +// selection would not. |
| +func TestMoveRoomCheckOnlyRefuses(t *testing.T) { |
| + const host, app = "acme.go.tito.io", "bab/titogo" |
| + ctx := context.Background() |
| + t.Run("room: the selection region HA off makes", func(t *testing.T) { |
| + off, err := haMoveFleet(t, false, nil).srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + on, err := haMoveFleet(t, true, nil).srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + if err != nil || on != off || on.Target != "uk-box" || on.Source != "ie-box" { |
| + t.Fatalf("region HA on %+v %v, off %+v", on, err, off) |
| + } |
| + }) |
| + t.Run("no room: the only destination is legacy", func(t *testing.T) { |
| + pages := pagesTo(t) |
| + fx := haMoveFleet(t, true, map[string]string{"uk-box": hostactors.RoleLegacy}) |
| + _, err := fx.srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + var full *noCapacityError |
| + if !errors.As(err, &full) || full.Region != "uk" { |
| + t.Fatalf("err = %v, want no room in uk", err) |
| + } |
| + if ref := claimResponseRefusal(err); ref.Status != http.StatusServiceUnavailable || ref.Msg != noCapacityRefusal("uk") { |
| + t.Fatalf("refusal %+v", ref) |
| + } |
| + expectPages(t, pages, noRoomSubject("uk")) |
| + }) |
| + t.Run("a legacy box beside a default one is not filtered out", func(t *testing.T) { |
| + roles := map[string]string{"uk-box": hostactors.RoleLegacy} |
| + off, err := haMoveFleet(t, false, roles, ukBox("uk-box-2")).srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + if err != nil || !off.Many { |
| + t.Fatalf("region HA off: %+v %v, want main's several-boxes refusal", off, err) |
| + } |
| + on, err := haMoveFleet(t, true, roles, ukBox("uk-box-2")).srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + if err != nil || on != off { |
| + t.Fatalf("region HA on %+v %v, want region HA off's %+v", on, err, off) |
| + } |
| + }) |
| + t.Run("a platform source still moves out", func(t *testing.T) { |
| + fx := haMoveFleet(t, true, map[string]string{"ie-box": hostactors.RolePlatform}) |
| + got, err := fx.srv.selectMoveBoxes(ctx, host, "uk", "ie", app) |
| + if err != nil || got.Source != "ie-box" || got.Target != "uk-box" { |
| + t.Fatalf("selection %+v %v", got, err) |
| + } |
| + }) |
| +} |
| + |
| +// Astra 2a round 1 finding 4: with host claims on and region HA off, the |
| +// move door is main's. A persisted role excludes nothing, and the door |
| +// reads no roster, so an unreadable one cannot refuse the move. |
| +func TestMoveDestinationWithRegionHAOffIsMains(t *testing.T) { |
| + const host, app = "acme.go.tito.io", "bab/titogo" |
| + fx := haMoveFleet(t, false, map[string]string{"uk-box": hostactors.RoleLegacy}) |
| + got, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app) |
| + if err != nil || got.Target != "uk-box" { |
| + t.Fatalf("selection %+v %v", got, err) |
| + } |
| + fx.srv.cfg.Releases = failingGet{Store: fx.rel, key: hostactors.Key} |
| + if got, err := fx.srv.selectMoveBoxes(context.Background(), host, "uk", "ie", app); err != nil || got.Target != "uk-box" { |
| + t.Fatalf("with the roster unreadable: %+v %v", got, err) |
| + } |
| +} |
| + |
| +// haMoveDoor is moveDoorWithClaim's world on a host-claim console with the |
| +// claim runtime armed: roomy edges files (uk-box's changed by edit), a |
| +// roster naming both regions and, with ha, region HA on. |
| +func haMoveDoor(t *testing.T, code string, ha bool, edit func(map[string]any)) (*consoleFixture, string, string) { |
| + t.Helper() |
| + fx, token, moveURL := moveDoorWithClaim(t, code) |
| + roomyEdges(t, fx, edit, "ie-box", "uk-box") |
| + haRoster(t, fx, nil, ieBox("ie-box"), ukBox("uk-box")) |
| + armClaimMoves(t, fx, true, true, nil) |
| + if ha { |
| + bindRegionHA(fx) |
| + } |
| + return fx, token, moveURL |
| +} |
| + |
| +// Astra 2a round 2 finding 1: through the whole HTTP door, a destination |
| +// with no box that has room (no edges file, or only blank public |
| +// addresses) is the no-room refusal, alert included, and writes nothing: |
| +// not the placement checks' 400. With region HA off it is main's 400; with |
| +// room the move is admitted to uk-box either way. |
| +func TestMoveDoorWithNoRoomInTheDestinationIsNoRoom(t *testing.T) { |
| + for _, c := range []struct { |
| + name string |
| + edit func(map[string]any) |
| + }{ |
| + {"only a blank public address", func(m map[string]any) { m["eip"] = "" }}, |
| + {"no edges file", nil}, |
| + } { |
| + t.Run(c.name, func(t *testing.T) { |
| + noFile := c.edit == nil |
| + world := func(t *testing.T, code string, ha bool) (*consoleFixture, string, string) { |
| + fx, token, moveURL := haMoveDoor(t, code, ha, c.edit) |
| + if noFile { |
| + // A box that never reported: no edges file and no status |
| + // snapshot. (A fresh snapshot with no edges file is an |
| + // incomplete read, pinned below.) |
| + for _, key := range []string{edgesFilePrefix + "uk-box.json", "status/boxes/uk-box.json"} { |
| + if err := fx.rel.Delete(context.Background(), key); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + return fx, token, moveURL |
| + } |
| + _, token, moveURL := world(t, "code-ha-move-off", false) |
| + if code, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`); code != http.StatusBadRequest { |
| + t.Fatalf("region HA off: move = %d %s, want main's 400", code, out) |
| + } |
| + pages := pagesTo(t) |
| + fx, token, moveURL := world(t, "code-ha-move-on", true) |
| + claim := claimBytes(t, fx, "acme.go.tito.io") |
| + code, hdr, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`) |
| + if code != http.StatusServiceUnavailable || !strings.Contains(out, noCapacityRefusal("uk")) { |
| + t.Fatalf("move = %d %s", code, out) |
| + } |
| + if got := hdr.Get("Retry-After"); got != noCapacityRetryAfter { |
| + t.Fatalf("Retry-After = %q, want %q", got, noCapacityRetryAfter) |
| + } |
| + expectPages(t, pages, noRoomSubject("uk")) |
| + if claimBytes(t, fx, "acme.go.tito.io") != claim { |
| + t.Fatal("a refused move wrote the claim") |
| + } |
| + a, err := hostops.ReadAdmission(context.Background(), fx.rel, "bab", "titogo", claimMoveAppID) |
| + if err != nil || len(a.Value.Registrations) != 0 { |
| + t.Fatalf("a refused move registered: %+v %v", a.Value.Registrations, err) |
| + } |
| + if rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io"); err != nil || rec.Move != nil { |
| + t.Fatalf("a refused move journaled: %+v %v", rec.Move, err) |
| + } |
| + }) |
| + } |
| + t.Run("room", func(t *testing.T) { |
| + fx, token, moveURL := haMoveDoor(t, "code-ha-move-room", true, nil) |
| + if code, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`); code != http.StatusAccepted { |
| + t.Fatalf("move = %d %s", code, out) |
| + } |
| + o, err := hostclaim.Read(context.Background(), fx.rel, "acme.go.tito.io") |
| + if err != nil || o.Claim == nil || o.Claim.Op.Intent["target"] != "uk-box" { |
| + t.Fatalf("claim %+v %v, want a move to uk-box", o.Claim, err) |
| + } |
| + }) |
| +} |
| + |
| +// Census ruling region-ha-2a rev 2 item 3: an incomplete placement read |
| +// (here a malformed edges file in the destination) is not no room. The |
| +// move door answers the existing 503 "temporarily unavailable", pages |
| +// nobody, and writes nothing. |
| +func TestMoveDoorWithAnIncompleteReadIsUnavailableNotNoRoom(t *testing.T) { |
| + for _, c := range []struct { |
| + name string |
| + break_ func(*testing.T, *consoleFixture) |
| + }{ |
| + {"a malformed edges file", func(t *testing.T, fx *consoleFixture) { |
| + key := edgesFilePrefix + "uk-box.json" |
| + _, etag, err := fx.rel.Get(context.Background(), key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := fx.rel.Put(context.Background(), key, []byte("{not json"), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + }}, |
| + {"a fresh status snapshot and no edges file", func(t *testing.T, fx *consoleFixture) { |
| + if err := fx.rel.Delete(context.Background(), edgesFilePrefix+"uk-box.json"); err != nil { |
| + t.Fatal(err) |
| + } |
| + }}, |
| + } { |
| + t.Run(c.name, func(t *testing.T) { moveDoorIncompleteRead(t, c.break_) }) |
| + } |
| +} |
| + |
| +func moveDoorIncompleteRead(t *testing.T, breakIt func(*testing.T, *consoleFixture)) { |
| + pages := pagesTo(t) |
| + fx, token, moveURL := haMoveDoor(t, "code-ha-move-incomplete", true, nil) |
| + ctx := context.Background() |
| + breakIt(t, fx) |
| + claim := claimBytes(t, fx, "acme.go.tito.io") |
| + code, hdr, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk"}`) |
| + if code != http.StatusServiceUnavailable || !strings.Contains(out, claimResponseUnavailable) || strings.Contains(out, noCapacityRefusal("uk")) { |
| + t.Fatalf("move = %d %s, want the temporarily unavailable 503", code, out) |
| + } |
| + if got := hdr.Get("Retry-After"); got == noCapacityRetryAfter { |
| + t.Fatalf("Retry-After = %q, the no-room value", got) |
| + } |
| + expectPages(t, pages) |
| + if claimBytes(t, fx, "acme.go.tito.io") != claim { |
| + t.Fatal("a refused move wrote the claim") |
| + } |
| + if rec, err := provision.GetRecord(ctx, fx.rel, "bab", "titogo", "acme.go.tito.io"); err != nil || rec.Move != nil { |
| + t.Fatalf("a refused move journaled: %+v %v", rec.Move, err) |
| + } |
| +} |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index a012e618b..dfbb38b41 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -797,7 +797,7 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| - seal = "57efed074effbf75c0179626dcec5026330081958e88e3b62c3f9c55a5df1944" |
| + seal = "fdfab9fb943e4e9678e3102b12020782ad181549b4a701d3267e799f986e92ae" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go| index 9f6ca38a5..d81d86a14 100644 |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| @@ -668,7 +668,7 @@ var plan9B8CensusInventory = []censusEntry{ |
| {"internal/console/apiinstancemove.go", "Server.moveTransitionSaying", "record-writer", "key:SetRecordMove", "d1f7162eecdc234607c708a83e9633cdf77cc6931bac6754fb51042dd8409377"}, |
| // Re-reviewed at the merge of carlos-run into seeded-instances (2026-10-13): main's door with the seed rule (refuseSeedMove) after its gate and before the transition; the transition is unchanged and refuses the same seed again under its CAS. |
| // Re-reviewed for home part 2, Task 5 (ruling owed): a home target is refused on the plain path (MV-N510) and for a home-named box (MV-N511) before any write, and admitted under host claims through startClaimModeMove; its record writes are unchanged. |
| - {"internal/console/apiinstancemove.go", "Server.handleAPIInstanceMoveRequest", "record-writer", "key:SetRecordMove,lit:instances", "79483d9603c096a269042d7b250e328761f9de7f365c272b82d34b16579b0417"}, |
| + {"internal/console/apiinstancemove.go", "Server.handleAPIInstanceMoveRequest", "record-writer", "key:SetRecordMove,lit:instances", "b672781c363eb29d5098ff3aa1cf09065a0673acc6f5ce283fd57b63abfe6db4"}, |
| {"internal/console/apiinstancemove.go", "Server.registerMovesPhase1Routes", "key-site", "lit:instances", "5af589d288ba22856f631481c40cc001aac342ea74cee6ee0464ec77d0320bbc"}, |
| {"internal/console/apiinstances.go", "Server.instanceManifest", "key-site", "lit:instances", "e6160ffe84abda9163527ccb33e2ffff2109b35908adb2f3a754534325658fdf"}, |
| {"internal/console/apiinstances.go", "Server.handleAPIInstanceList", "key-site", "lit:instances", "558d0b3e7b0f06faa8e5067ff6f8c131eb899f13d08b71218dfb2454c3e319e1"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv| index 1299a57bd..16a63946e 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2a.tsv |
| @@ -11,6 +11,7 @@ |
| # Task 10 (system.go): the box and group verbs add `box` and `group` to systemVerb's verb list and two cases to runSystem's dispatch, calling runSystemBoxVerb and runSystemGroupVerb (system_roles.go). The two seed-overlay authority declarations are re-digested, class, task, basis and (empty) kinds unchanged: no root read is added, and the verbs reach the console over the operator's CLI session. |
| # Task 11: H27 on the create doors (internal/console nocapacity.go; hostops chosen.go): beginClaimCreate chooses the executor before it registers anything (chooseExecutorFirst) and carries the choice to the create (hostops.WithChosenExecutor, honoured by ProductionAuthority.SelectExecutor), claimResponseRefusal answers no room with a 503 on instance create, domain attach's first route (attachClaimFirstRoute, unchanged) and standalone sidecar create, and handleAPIInstanceCreate judges an app-derived region by the chooser's eligible set. handleAPIInstanceCreate chooses before its first write, the environment binding (chooseCreateFirst), and beginClaimCreate keeps that choice; with region HA on, an explicit region with no box that has room is the no-room refusal before the armed-placement gate (roomIn). Census ruling region-ha-2a covers two accepted-legacy units: Server.beginClaimCreate re-frozen (one call added before RegisterStart, its one root-read site, the claim read file:Read@470, unchanged), and Server.chooseCreateFirst, new: the same claim read, made before the binding, feeding only the placement choice; it returns its choice with the claim observation it decided on, and beginClaimCreate keeps the choice only if its own read is that observation (ruling item 5). The seal moves. |
| # Task 11 fix round 1: an incomplete placement read is not "no room" (controller ruling). hostops.ExecutorChooser's Choose, Permits and PlaceableBoxes ask judged, which wraps the chooser's own Edges read and answers ErrEvidenceIncomplete when that snapshot is incomplete; ExecutorChooser.eligible (accepted-legacy, merge ruling 2) is unchanged. The no-capacity alert is recorded only once sent (noCapacityAlerts.failed gives a failed send's slot back), and the no-room 503 carries Retry-After: 60 (setNoCapacityRetryAfter in writeClaimResponseError, renderDoorRefusal and handleAPIDomainAttach's refusal write). Envelope-only: no root-read unit changed or was added, and the legacy seal stands. |
| +# Task 12: H27 on the move door's destination, through hostops.ExecutorChooser.PlaceableBoxes and only with region HA on: roomIn refuses a destination with no box that has room before the placement checks (no edges file, blank public addresses), and moveRoomCheck (internal/console boxselect.go) refuses when none of selectMoveBoxes' destination candidates is placeable. Both only refuse: the candidates and the destination are main's. An incomplete placement read (hostops.ErrEvidenceIncomplete) is the temporarily unavailable 503, never no room. Census ruling region-ha-2a is asked to re-freeze one accepted-legacy unit, Server.handleAPIInstanceMoveRequest: two refusals added before any write; its read sites unchanged, offsets moved. No write site is added or changed. The seal moves. |
| internal/activator/activator.go type Service authority B,C,D,F,J4 rev3=PROPOSED-seed; existing activator lifecycle and due-wake state plus owned seed exclusion and incarnation-keyed diagnostics; normal refusal and preparation ordering remain authoritative be4ca3a3ea2bd768e7e52c1056513d4fef1c1702d2e04656c540cdcbb858865c |
| @coverage internal/lbhealth ad1211a01a4eb3d02cd8ed95cbff0f8b17b3976ce9c0279ebd2cae2f20c07124 |
| @coverage internal/hostactors 6736698380a247764ada687f2476f89382fc32a1183a50c06afe75967d716eab |
| @@ -21,7 +22,7 @@ internal/activator/activator.go type Service authority B,C,D,F,J4 rev3=PROPOSED- |
| @coverage internal/edge 20177c9e7dc961937677bbf2763c73004d7c95f68512591800f6fe841c36152f |
| internal/host/edgesfile.go edgesFileWriteClaims authority F,J1 root-route;kept=F;scope=protected-decision-only registry:Account@10835,registry:Alias@5297,registry:Alias@5332,registry:Alias@5754,registry:Alias@5911,registry:Alias@5981,registry:Alias@6013,registry:Alias@6482,registry:Alias@8772,registry:App@10847,registry:DNSStatus@5429,registry:DNSStatus@5920,registry:Host@10480,registry:Host@2878,registry:Host@5389,registry:Host@6234,registry:Host@6567,registry:Host@8819,registry:Host@8866,registry:Host@8916,registry:Host@8951,registry:Host@9000,registry:Platform@2921,registry:Provisioned@2935,registry:Replica@2959,registry:Status@877,whole:Route@3212,whole:Route@323,whole:Route@3505,whole:Route@351,whole:RouteAlias@5282,whole:RouteAlias@8725,whole:RoutePath@3520,whole:WildcardDomain@8905 8c7cdffba66886b92f9a2ac663a86dbdd5f80699afd2e6afb4571da916c727ad |
| @coverage internal/activator fd9cf14712f55895541bb8bfbf8137cfd446298bcaa8634b0aba7b8344c29ec3 |
| -@coverage internal/console 97ee054fd00a99d3d97cb0b7a1d3292d03711063e1d709257b9544e5730cf368 |
| +@coverage internal/console 7187e2cb5803a942616df8d349a87be050a65be35cfdaf66493b0f4112a02387 |
| @coverage . 0e6e4542758248000de1882a01f13e3449c880b4ae23e8b2ff0d6dce076b614c |
| @legacy b02b534b6617f57381ba12e9d9a2bf1bd63f737dc68005de926ff18b7db93dd3 internal/console/apiboxroles.go Server.boxRoster legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2a file:Read@484 9be9925de14dbbd7d86123bf9faf1535c7b116575cadcb8ba983a665af0513f4 |
| internal/console/apiboxroles.go Server.boxRoster legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2a file:Read@484 9be9925de14dbbd7d86123bf9faf1535c7b116575cadcb8ba983a665af0513f4 |
| @@ -31,3 +32,5 @@ system.go systemVerb authority B,C,D,F,J4 rev3=PROPOSED-seed; command arguments, |
| internal/console/claim_create.go Server.beginClaimCreate legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2a file:Read@470 ff9b246aa3dcfe180ea1901ecd9fc1b3b18561dc309e08c6a922870d89179705 |
| @legacy 5aa99a1b92eb73601585a25265c5285bcfd0c6b6ae7f402248eb1a299bc79831 internal/console/nocapacity.go Server.chooseCreateFirst legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2a file:Read@271 7cc51e7812cc59f39ea23d5285c621d912557f4890d6e76d566f6182b0fe3ffd |
| internal/console/nocapacity.go Server.chooseCreateFirst legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2a file:Read@271 7cc51e7812cc59f39ea23d5285c621d912557f4890d6e76d566f6182b0fe3ffd |
| +@legacy d7a8539af8e4f3123718211dcc8ba59c6620de84ef93c18a8ddb22686d2a0c4c internal/console/apiinstancemove.go Server.handleAPIInstanceMoveRequest legacy accepted-legacy accepted-legacy;rev3=census-ruling-region-ha-2a;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with two refusals added before any write, both no room in the destination (H27): roomIn before the placement checks and moveRoomCheck's through selectMoveBoxes; read sites unchanged, offsets moved file:Exec@10024,file:Exec@10201,file:Exec@10352,file:Exec@11352,file:Exec@8945,file:Exec@9033,file:Read@7431,file:Read@9743 b672781c363eb29d5098ff3aa1cf09065a0673acc6f5ce283fd57b63abfe6db4 |
| +internal/console/apiinstancemove.go Server.handleAPIInstanceMoveRequest legacy accepted-legacy accepted-legacy;rev3=census-ruling-region-ha-2a;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with two refusals added before any write, both no room in the destination (H27): roomIn before the placement checks and moveRoomCheck's through selectMoveBoxes; read sites unchanged, offsets moved file:Exec@10024,file:Exec@10201,file:Exec@10352,file:Exec@11352,file:Exec@8945,file:Exec@9033,file:Read@7431,file:Read@9743 b672781c363eb29d5098ff3aa1cf09065a0673acc6f5ce283fd57b63abfe6db4 |