carlos / platform Public

host: layoutStatusReport wires the armed pass into the status snapshot (plan 4, task 5, step 2)

statusLoop passes routes through layoutStatusReport, which reads the
published layout pass and the last intent layoutLeg saw: nil with no
pass armed, otherwise every instance-kind route's word from the pass's
own Classify (foreign/fenced/conflict carry the reason), external for
an unflagged socket route, sidecars and non-instance kinds excluded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 08c4484f2ed8348c041605b6a6d24bd92337d05e parent d16a8ae
3 files changed, +143 −0
  • internal/host/agent.go +5 −0
  • internal/host/agent_layout_test.go +94 −0
  • internal/host/layoutleg.go +44 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index b25217229..c3e2a54ca 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -2749,6 +2749,11 @@ func statusLoop(reg *registry.Registry) {
// One stat (two with a WAL) per database route per tick, which
// is what `carlos instances list` and `carlos routes` show as DB.
DBBytes: dbFileBytes,
+ // Layout is nil until this box has armed a layout pass (an
+ // activator running, past startup recovery) — see
+ // layoutStatusReport's own doc comment for why nil is the
+ // right absence to publish rather than a guessed mode.
+ Layout: layoutStatusReport(routes),
}
// A fleet box (CARLOS_FLEET set) never gets here as a contender: it
diff --git a/internal/host/agent_layout_test.go b/internal/host/agent_layout_test.go
index f8f397952..2e5bb2f46 100644
--- a/internal/host/agent_layout_test.go
+++ b/internal/host/agent_layout_test.go
@@ -454,3 +454,97 @@ func TestSyncExecSerializedSkipsAHostWithALayoutJournal(t *testing.T) {
t.Fatalf("sibling bundle = %q, %v", b, err)
}
}
+
+// TestLayoutStatusReportWithNoPassArmedIsNil pins the snapshot writer's
+// omission rule (R15): with no activator, and so no layout pass, there is
+// nothing to move and nothing to report — the status snapshot must omit the
+// block entirely rather than guess at a mode.
+func TestLayoutStatusReportWithNoPassArmedIsNil(t *testing.T) {
+ resetLayoutGlobals(t)
+ if got := layoutStatusReport(nil); got != nil {
+ t.Fatalf("report = %+v, want nil with no pass armed", got)
+ }
+}
+
+// TestLayoutStatusReportModeFollowsTheLastIntentSeen pins the three-way
+// Mode mapping the amendment (R15) puts on the report, distinct from the
+// brief's original two-word doc: "none" before any intent is recorded, the
+// mode itself once one is, and "unreadable" when the last read of it
+// failed — never silently downgraded to "none", which would read as "this
+// box wants flat" when the box in fact could not tell.
+func TestLayoutStatusReportModeFollowsTheLastIntentSeen(t *testing.T) {
+ resetLayoutGlobals(t)
+ dd, sd := t.TempDir(), t.TempDir()
+ armedLayoutPass.Store(&layout.Pass{DataDir: dd, SocketDir: sd})
+
+ if got := layoutStatusReport(nil); got == nil || got.Mode != "none" {
+ t.Fatalf("mode with no intent seen = %+v, want none", got)
+ }
+ noteLayoutIntent(release.LayoutModeFlat)
+ if got := layoutStatusReport(nil); got == nil || got.Mode != "flat" {
+ t.Fatalf("mode after a flat intent = %+v, want flat", got)
+ }
+ noteLayoutIntent(release.LayoutModeNested)
+ if got := layoutStatusReport(nil); got == nil || got.Mode != "nested" {
+ t.Fatalf("mode after a nested intent = %+v, want nested", got)
+ }
+ noteLayoutIntent(string(layout.IntentUnreadable))
+ if got := layoutStatusReport(nil); got == nil || got.Mode != "unreadable" {
+ t.Fatalf("mode after an unreadable read = %+v, want unreadable", got)
+ }
+}
+
+// TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes pins R15's row
+// selection: an ActivatorManaged row goes through the pass's own Classify
+// (proven here by a row shaped to classify as foreign, so the reason comes
+// through too), an unflagged socket instance route reports "external", and
+// everything else — a sidecar, a static route, a service route, and an
+// instance route with no socket at all — carries no row, because the
+// report is not the place any of those are decided.
+func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) {
+ resetLayoutGlobals(t)
+ dd, sd := t.TempDir(), t.TempDir()
+ armedLayoutPass.Store(&layout.Pass{DataDir: dd, SocketDir: sd})
+ noteLayoutIntent(release.LayoutModeNested)
+
+ routes := []registry.Route{
+ // ActivatorManaged, but its paths match neither default — Classify
+ // must be the thing deciding this, not a guess made here.
+ {Host: "managed.example.com", Kind: "instance", Hibernate: true,
+ Socket: "/elsewhere/managed.sock", DBPath: "/elsewhere/managed.db", Status: "ready"},
+ // Hibernate=false with a socket: externally supervised.
+ {Host: "external.example.com", Kind: "instance", Hibernate: false, Socket: "/elsewhere/external.sock"},
+ // A sidecar, explicitly out of scope (R15, finding S6).
+ {Host: "side.example.com", Kind: "sidecar"},
+ // Not instance-kind at all.
+ {Host: "static.example.com", Kind: "static"},
+ {Host: "svc.example.com", Kind: "service"},
+ // Instance-kind but no socket and not hibernate-flagged — neither
+ // managed nor the "unflagged socket" shape.
+ {Host: "nosock.example.com", Kind: "instance"},
+ }
+
+ report := layoutStatusReport(routes)
+ if report == nil {
+ t.Fatal("report = nil, want one with a pass armed")
+ }
+ if report.Mode != "nested" {
+ t.Errorf("mode = %q, want nested", report.Mode)
+ }
+ if len(report.Rows) != 2 {
+ t.Fatalf("rows = %+v, want exactly managed and external", report.Rows)
+ }
+ managed := report.Rows["managed.example.com"]
+ if managed.Layout != "foreign" || managed.Reason == "" {
+ t.Errorf("managed row = %+v, want foreign with a reason from Classify", managed)
+ }
+ external := report.Rows["external.example.com"]
+ if external.Layout != "external" || external.Reason != "" {
+ t.Errorf("external row = %+v, want external with no reason", external)
+ }
+ for _, host := range []string{"side.example.com", "static.example.com", "svc.example.com", "nosock.example.com"} {
+ if _, ok := report.Rows[host]; ok {
+ t.Errorf("%s has a row, want none", host)
+ }
+ }
+}
diff --git a/internal/host/layoutleg.go b/internal/host/layoutleg.go
index caf6dff18..edeb82ccb 100644
--- a/internal/host/layoutleg.go
+++ b/internal/host/layoutleg.go
@@ -21,6 +21,7 @@ import (
"github.com/carlosframework/platform/internal/layout"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
)
// armedLayoutPass is the box's one layout pass. RunAgent stores it once,
@@ -51,6 +52,49 @@ func lastLayoutIntent() string {
func noteLayoutIntent(mode string) { layoutIntentSeen.Store(&mode) }
+// layoutStatusReport builds the box's layout report for the status
+// snapshot (design §8.4, amendment R15): the mode this box last converged
+// to, and every instance-kind route's word from the armed pass's own
+// Classify — the same classification a wake would see, so status never
+// says something different from what the activator actually does.
+//
+// nil means no activator is armed on this box, so there is no pass and
+// nothing to report; the caller (statusLoop) leaves Probes.Layout nil,
+// which CollectBox carries through as an absent key, never a guessed mode.
+//
+// routes is the same list the caller already read for its other probes —
+// this never lists the registry itself, mirroring every other probe in
+// this file.
+func layoutStatusReport(routes []registry.Route) *status.LayoutReport {
+ pass := armedLayoutPass.Load()
+ if pass == nil {
+ return nil
+ }
+ mode := lastLayoutIntent()
+ if mode == "" {
+ mode = "none"
+ }
+ rows := map[string]status.RowLayout{}
+ for _, rt := range routes {
+ // Only instance-kind rows: a sidecar (Kind "sidecar") is outside
+ // the pass entirely (finding S6), and so is everything that is
+ // neither an instance nor a sidecar (static, service).
+ if rt.Kind != "instance" {
+ continue
+ }
+ switch {
+ case activator.ActivatorManaged(rt):
+ word, reason := pass.Classify(rt, routes)
+ rows[rt.Host] = status.RowLayout{Layout: string(word), Reason: reason}
+ case rt.Socket != "" && !rt.Hibernate:
+ // ActivatorManaged's own doc: an unflagged socket route is run
+ // by another supervisor, nobody's to migrate or isolate.
+ rows[rt.Host] = status.RowLayout{Layout: "external"}
+ }
+ }
+ return &status.LayoutReport{Mode: mode, Rows: rows}
+}
+
// reloadRouting asks the live edge to rebuild its routes now, if there is
// one yet.
func reloadRouting() {