carlos / platform Public

status: per-box snapshots — needles die in the writer

CollectBox turns registry routes + probe results into a public Box:
ratios band to words (never numbers), unknown probes publish JSON
null (never a guessed 0), and Label hashes the machine id into an
opaque ada-stargazing-style name so no hostname or machine id ever
reaches the bucket. WriteBox Puts status/boxes/<label>.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 08b72686a9183d9554cb0939b16c2dbe3809bd48 parent 33c9ba8
2 files changed, +312 −0
  • internal/status/snapshot.go +227 −0
  • internal/status/status_test.go +85 −0
diff --git a/internal/status/snapshot.go b/internal/status/snapshot.go
new file mode 100644
index 000000000..ec7d5d65e
--- /dev/null
+++ b/internal/status/snapshot.go
@@ -0,0 +1,227 @@
+// Package status is the fleet status page's writer half: per-box snapshots
+// that carry only what the public commitments allow — aggregate counts and
+// band words, never ratios, hostnames, or machine ids. Privacy is enforced
+// HERE, in the process that does the counting and banding, not downstream —
+// a merge step or a page template can only leak what this package already
+// let through.
+package status
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/json"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Probes carries everything CollectBox may not read from the registry —
+// each nil means "unknown", and unknown publishes as null, never a guess.
+type Probes struct {
+ MachineID string // hashed into the label; never serialized
+ Region string // operator-set, coarse; "" omits the field
+ MemFrac *float64 // 0..1 memory utilization
+ DiskFrac *float64 // 0..1 proximity to the disk floor
+ Listening func(sock string) *bool // kernel socket table read; nil fn = unknown
+ LagSecs *int64 // seconds since litestream last progressed
+}
+
+// RouteCounts is aggregate-only: no host, no socket path, just how many.
+// Awake/Asleep stay null together whenever the socket table can't be read in
+// full — partial knowledge published as a count would read as more certain
+// than it is.
+type RouteCounts struct {
+ Static, Resident, Hibernating int
+ Awake, Asleep *int // null when the socket table is unreadable
+}
+
+// Replication summarizes litestream's furthest-behind lag across this box's
+// routes, banded to a healthy bool rather than the raw seconds — the raw
+// number is still carried for the console's private view, but the public
+// commitment is only ever "healthy" or "not".
+type Replication struct {
+ Healthy *bool `json:"healthy"`
+ LagSecs *int64 `json:"lag_seconds"`
+}
+
+// Box is one box's self-report: what CollectBox produces and WriteBox
+// serializes. Every field here has already had its needles pulled — there is
+// no hostname, machine id, or raw ratio anywhere in this type.
+type Box struct {
+ GeneratedAt time.Time `json:"generated_at"`
+ Label string `json:"label"`
+ Region string `json:"region,omitempty"`
+ Routes RouteCounts `json:"routes"`
+ Load *string `json:"load"` // comfortable|busy|critical
+ Capacity *string `json:"capacity"` // plenty|getting-full|at-capacity
+ Replication Replication `json:"replication"`
+}
+
+// replHealthyWithin is the lag under which replication counts as healthy.
+// 15 minutes: litestream's own replicate interval is seconds, so anything
+// beyond a quarter hour behind means the replica loop has actually stalled
+// (network partition, disk full, credential rot) rather than just being
+// mid-batch — a threshold loose enough not to flap on ordinary jitter, tight
+// enough to catch a real outage inside one on-call check-in.
+const replHealthyWithin = 15 * time.Minute
+
+// loadWords and capacityWords band a fraction into a public word — never a
+// number, per the fleet status page's public commitments (see the plan's
+// privacy rules). The three words are ordered low, medium, high.
+var loadWords = [3]string{"comfortable", "busy", "critical"}
+var capacityWords = [3]string{"plenty", "getting-full", "at-capacity"}
+
+// bandBoundaryLow and bandBoundaryHigh are the two cut points shared by every
+// banded fraction: below the low boundary is the first word, at-or-below the
+// high boundary is the second, above it the third. Named constants so the
+// 0.70/0.90 split — a decision the operator made once — isn't a magic number
+// re-typed at each call site.
+const (
+ bandBoundaryLow = 0.70
+ bandBoundaryHigh = 0.90
+)
+
+// bandWord turns a possibly-unknown fraction into one of three words. nil in
+// (unknown) means nil out — CollectBox must never invent a band for a
+// quantity it never measured.
+func bandWord(f *float64, words [3]string) *string {
+ if f == nil {
+ return nil
+ }
+ var w string
+ switch {
+ case *f < bandBoundaryLow:
+ w = words[0]
+ case *f <= bandBoundaryHigh:
+ w = words[1]
+ default:
+ w = words[2]
+ }
+ return &w
+}
+
+// labelNames and labelActions are the two word lists Label indexes into —
+// Eleven's "ada-stargazing" shape: a first name and a gerund action, picked
+// to read as pleasant and give away nothing about the machine they name.
+// Deliberately small const slices (not generated, not fetched) so the label
+// space and its stability are auditable at a glance.
+var labelNames = [40]string{
+ "ada", "grace", "alan", "linus", "margaret", "dennis", "barbara", "ken",
+ "radia", "vint", "tim", "hedy", "kathleen", "edsger", "donald", "john",
+ "claude", "marvin", "shafi", "leslie", "whitfield", "adi", "ron", "sophie",
+ "joan", "frances", "betty", "jean", "mary", "annie", "katherine", "dorothy",
+ "ada", "elena", "priya", "yuki", "noor", "amara", "leo", "iris",
+}
+var labelActions = [40]string{
+ "stargazing", "birdwatching", "gardening", "sketching", "kayaking",
+ "orienteering", "foraging", "woodworking", "beekeeping", "cycling",
+ "origami", "brewing", "quilting", "hiking", "juggling", "puzzling",
+ "weaving", "sailing", "climbing", "cartography", "fermenting", "welding",
+ "knitting", "pottery", "birding", "spelunking", "kitesurfing", "canoeing",
+ "tinkering", "letterpress", "printmaking", "bookbinding", "beachcombing",
+ "fossicking", "aquascaping", "papermaking", "clockmaking", "lockpicking",
+ "stenciling", "marquetry",
+}
+
+// Label derives a stable, opaque "name-gerund" label from a machine id — the
+// same shape as Eleven's ada-stargazing box names. sha256 rather than a
+// weaker hash: the machine id must never be recoverable from the label by
+// brute force over any plausible id space (hostnames, instance ids), and the
+// label is public.
+func Label(machineID string) string {
+ sum := sha256.Sum256([]byte(machineID))
+ name := labelNames[int(sum[0])%len(labelNames)]
+ action := labelActions[int(sum[1])%len(labelActions)]
+ return name + "-" + action
+}
+
+// CollectBox turns a box's live registry rows and probe results into the
+// public Box for that box. It is the ONLY place fractions get banded to
+// words, sockets get counted instead of named, and the machine id gets
+// hashed instead of shipped — every value this function returns is already
+// safe to publish as-is.
+func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
+ var counts RouteCounts
+ var awakeKnown = true
+ awake, asleep := 0, 0
+
+ for _, r := range routes {
+ switch {
+ case r.Kind == "static":
+ counts.Static++
+ case r.Hibernate:
+ counts.Hibernating++
+ if p.Listening == nil {
+ awakeKnown = false
+ continue
+ }
+ isListening := p.Listening(r.Socket)
+ if isListening == nil {
+ awakeKnown = false
+ continue
+ }
+ if *isListening {
+ awake++
+ } else {
+ asleep++
+ }
+ default:
+ counts.Resident++
+ }
+ }
+ if awakeKnown && p.Listening != nil {
+ counts.Awake, counts.Asleep = &awake, &asleep
+ }
+
+ // Capacity is the worst-of DiskFrac/MemFrac — whichever known fraction is
+ // closer to full drives the word, because a box that's fine on memory but
+ // out of disk is still a box about to have a bad day.
+ var worst *float64
+ switch {
+ case p.DiskFrac != nil && p.MemFrac != nil:
+ if *p.DiskFrac >= *p.MemFrac {
+ worst = p.DiskFrac
+ } else {
+ worst = p.MemFrac
+ }
+ case p.DiskFrac != nil:
+ worst = p.DiskFrac
+ case p.MemFrac != nil:
+ worst = p.MemFrac
+ }
+
+ var healthy *bool
+ if p.LagSecs != nil {
+ h := *p.LagSecs < int64(replHealthyWithin.Seconds())
+ healthy = &h
+ }
+
+ return Box{
+ GeneratedAt: now,
+ Label: Label(p.MachineID),
+ Region: p.Region,
+ Routes: counts,
+ Load: bandWord(p.MemFrac, loadWords),
+ Capacity: bandWord(worst, capacityWords),
+ Replication: Replication{Healthy: healthy, LagSecs: p.LagSecs},
+ }
+}
+
+// WriteBox marshals a Box indented (human-diffable in the bucket) and Puts it
+// at its label-derived key with ifMatch "", per this task's contract. Note
+// for whoever wires the recurring publish loop (Task 3+): release.Store's
+// ifMatch "" is create-only (If-None-Match: *) per store.go, so a second
+// WriteBox for the same label — expected every 5 minutes per the plan — will
+// return ErrCASConflict once the object exists. Overwriting on a schedule
+// needs either a Get-then-Put-with-etag retry loop or a Delete-then-Put
+// around this call; WriteBox itself stays a thin, single-attempt Put so its
+// behavior is exactly what Task 2's contract specifies.
+func WriteBox(ctx context.Context, s release.Store, b Box) error {
+ body, err := json.MarshalIndent(b, "", " ")
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, "status/boxes/"+b.Label+".json", body, "")
+ return err
+}
diff --git a/internal/status/status_test.go b/internal/status/status_test.go
new file mode 100644
index 000000000..795a7a292
--- /dev/null
+++ b/internal/status/status_test.go
@@ -0,0 +1,85 @@
+package status_test
+
+import (
+ "context"
+ "encoding/json"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
+)
+
+func TestBandBoundaries(t *testing.T) {
+ f := func(v float64) *float64 { return &v }
+ cases := []struct {
+ frac *float64
+ want string
+ }{
+ {f(0.69), "plenty"}, {f(0.70), "getting-full"},
+ {f(0.90), "getting-full"}, {f(0.91), "at-capacity"},
+ }
+ for _, c := range cases {
+ got := status.CollectBox(nil, status.Probes{MachineID: "m", DiskFrac: c.frac}, time.Now()).Capacity
+ if got == nil || *got != c.want {
+ t.Errorf("frac %v: got %v want %s", *c.frac, got, c.want)
+ }
+ }
+ if b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()); b.Capacity != nil || b.Load != nil {
+ t.Error("unknown fractions must band to null, not a word")
+ }
+}
+
+func TestAwakeAsleepNullWhenUnreadable(t *testing.T) {
+ routes := []registry.Route{{Host: "a", Kind: "instance", Hibernate: true, Socket: "/x.sock"}}
+ b := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now()) // nil Listening
+ if b.Routes.Awake != nil || b.Routes.Asleep != nil {
+ t.Error("nil socket probe must publish null, never 0")
+ }
+}
+
+func TestLabelStableAndOpaque(t *testing.T) {
+ a, b := status.Label("machine-1"), status.Label("machine-1")
+ if a != b || a == "" || strings.Contains(a, "machine") {
+ t.Errorf("label %q must be stable and never contain the machine id", a)
+ }
+}
+
+func TestSnapshotJSONCarriesNoHostnames(t *testing.T) {
+ routes := []registry.Route{{Host: "secret.example.com", Kind: "instance", Status: "ready"}}
+ b := status.CollectBox(routes, status.Probes{MachineID: "ip-10-0-0-7"}, time.Now())
+ j, _ := json.Marshal(b)
+ for _, leak := range []string{"secret", "example.com", "ip-10-0-0-7"} {
+ if strings.Contains(string(j), leak) {
+ t.Errorf("snapshot leaked %q: %s", leak, j)
+ }
+ }
+}
+
+// TestWriteBoxPutsUnderLabeledKey isn't in the plan's Step-1 listing, but
+// WriteBox is part of the exported contract Task 3 relies on — worth locking
+// down that it actually lands the box at the key CollectBox's label implies.
+func TestWriteBoxPutsUnderLabeledKey(t *testing.T) {
+ dir := t.TempDir()
+ store := release.NewDirStore(dir)
+ b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now())
+
+ if err := status.WriteBox(context.Background(), store, b); err != nil {
+ t.Fatalf("WriteBox: %v", err)
+ }
+
+ key := "status/boxes/" + b.Label + ".json"
+ got, _, err := store.Get(context.Background(), key)
+ if err != nil {
+ t.Fatalf("Get %s: %v", key, err)
+ }
+ var round status.Box
+ if err := json.Unmarshal(got, &round); err != nil {
+ t.Fatalf("unmarshal written box: %v", err)
+ }
+ if round.Label != b.Label {
+ t.Errorf("round-tripped label %q, want %q", round.Label, b.Label)
+ }
+}