carlos / platform Public

status: e2e + docs โ€” the feature is the page, the page is the contract

Extends the existing end-to-end smoke test with a fourth leg: a kind=status
route registered on the same edge, one publish tick composed from
internal/status's exported seam (CollectBox -> WriteBox -> BuildFleet ->
PublishFleet โ€” statusTick itself is unexported in internal/host, so the
test reproduces its steps directly, noted in a comment), then a GET through
the edge asserting 200 + "Fleet status" + a fleet.json that decodes cleanly
into status.Fleet. A second test exercises carlos status -dry-run via
status.DryRun directly (the CLI wrapper is three lines of flag parsing;
the function is the substance).

Adds a root README.md's "Fleet status" section โ€” what publishes, the three
privacy commitments, the full CARLOS_STATUS_*/STATUS_* config surface, the
dry-run verb, and the honest aws s3 cp fallback for a fully-private
deployment with no console. Cross-references the status design spec from
the platform design spec's testing section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 2ca2550f3960f514d184c31c310c52ed5746062a parent 3685590
3 files changed, +140 −2
  • README.md +78 −0
  • docs/superpowers/specs/2026-08-01-carlos-platform-design.md +3 −0
  • e2e_test.go +59 −2
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..234606688
--- /dev/null
+++ b/README.md
@@ -0,0 +1,78 @@
+# ๐Ÿค– CARLOS platform
+
+> ๐Ÿค– This document was written by an LLM (Claude), on the ideas,
+> instruction, and editing of humans, per
+> [factor X](https://11factor.org/#x).
+
+A black-box deployment platform for CARLOS apps: `carlos ship` publishes an
+immutable, content-addressed release; the platform places it on instances,
+routes to it through a shared edge, and surrounds it with platform-level
+glue. One deployment is **one object-store bucket + N hosts running the
+`carlos` binary + DNS** โ€” the whole self-hosting story in one sentence. See
+[`docs/superpowers/specs/2026-08-01-carlos-platform-design.md`](docs/superpowers/specs/2026-08-01-carlos-platform-design.md)
+for the full design.
+
+## Build & test
+
+ make build # CGO_ENABLED=0 build of the carlos binary
+ make test # go vet, gofmt -l, go test ./...
+
+One static binary plays every role, selected by subcommand: `carlos edge`,
+`carlos agent` (edge + activator + release adoption + fleet status, one
+process per host), `carlos ship`/`promote`/`rollback`, `carlos vet`. Run
+`carlos` with no arguments for the full command list.
+
+## Fleet status
+
+Every deployment computes a fleet status view: how many instances are
+running, how busy the boxes are, whether replication is healthy โ€” the
+CARLOS analogue of Eleven's own status pages. Each box's agent (`carlos
+agent`) writes its own snapshot to the deployment bucket every 5 minutes;
+whichever box holds the status lease that tick merges every snapshot into
+`status/fleet.json` plus the embedded viewer (`status/index.html`,
+`status/status.css`) and republishes all three. The edge serves them back
+out through a `kind=status` route, bucket-backed with a short cache โ€” no
+release machinery involved, no CAS churn, just "read the key, cache it
+briefly, serve it."
+
+Three commitments hold end to end, enforced in the writer rather than the
+page: the page publishes only fleet-wide aggregates and band words
+(`comfortable`/`busy`/`critical`, `plenty`/`getting-full`/`at-capacity`) โ€”
+never raw ratios, hostnames, or machine ids. An unknown reading publishes
+loudly as `null`, and a box that has stopped reporting renders `stale`,
+never silently dropped. App names are opt-in only, via `carlos add
+-status-named`, and everything else folds into one honest `apps_unlisted`
+count, so the aggregate never lies by omission.
+
+**Config surface**, all read by `carlos agent`'s status loop; an unset
+`CARLOS_STATUS_BUCKET` disables the loop entirely โ€” inert until configured,
+the same convention litestream replication uses:
+
+- `CARLOS_STATUS_BUCKET` / `CARLOS_STATUS_PREFIX` / `CARLOS_STATUS_REGION`
+ โ€” the bucket each box writes its snapshot to and, when it wins the lease,
+ publishes the merged feed to; prefix and region also feed the cross-box
+ status-lease election.
+- `STATUS_BOX_REGION` โ€” an operator-set, coarse region word (e.g. `"EU ยท
+ Ireland"`) carried onto this box's snapshot verbatim; unset omits the
+ field. Free text the operator chooses, never derived from cloud metadata.
+- `STATUS_HOST` / `STATUS_PUBLIC` โ€” not read by the agent process itself;
+ consumed by ops/tofu to decide whether the edge fronts the bucket's
+ `status/` prefix with a public hostname. `STATUS_PUBLIC=1` publishes the
+ page at `STATUS_HOST`; leaving it unset keeps the feed private, visible
+ only to console operators (`console grant -operator <address>`).
+
+**Dry run.** Before pointing a binary at a real bucket, `carlos status
+-dry-run <dir>` runs the whole publish pipeline โ€” `CollectBox` โ†’
+`WriteBox` โ†’ `BuildFleet` โ†’ `PublishFleet` โ€” against a scratch directory
+stuffed with fixture secrets (hostnames, a machine id, mid-band fractions),
+then reads every object it just published back out and fails loudly if any
+of them leaked. There is no live-publish CLI verb; the 5-minute loop inside
+`carlos agent` is the only publisher.
+
+**Honest limit.** A fully-private deployment (`STATUS_PUBLIC` unset) whose
+console happens to be down has no status page at all โ€” the operator's
+fallback is reading the object straight out of the bucket:
+
+ aws s3 cp s3://<bucket>/status/fleet.json -
+
+That's the whole story; nothing papers over it.
diff --git a/docs/superpowers/specs/2026-08-01-carlos-platform-design.md b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
index bb2da93c0..918bfb0ca 100644
--- a/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
+++ b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md
@@ -401,6 +401,9 @@ Assertions that fail builds, not paragraphs:
every engine it declares); additive-migration check between its last two
releases; park/wake round-trip. Passing vet makes an app's promises
checkable before adoption rather than discovered in production.
+- **The fleet status page is contract-tested the same way.** Allowlisted
+ JSON schemas and a `carlos status -dry-run` leak self-check pin the
+ privacy boundary shut โ€” see [Fleet status page โ€” design](2026-08-01-fleet-status-page-design.md).
## 10. Non-goals (v1)
diff --git a/e2e_test.go b/e2e_test.go
index 1efa33faf..3607ebc8e 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -1,7 +1,8 @@
// End-to-end smoke of the release/serve/rollback loop, AWS-free: a DirStore
// deployment bucket, a plain-HTTP edge on a TCP port, a real compiled tenant
-// (testdata/echoapp) behind a TCP-addr route, and a static site behind a
-// channel-tracked static route. Host-side adoption (release.Pass, symlink
+// (testdata/echoapp) behind a TCP-addr route, a static site behind a
+// channel-tracked static route, and the fleet status page behind a
+// bucket-backed kind=status route. Host-side adoption (release.Pass, symlink
// repointing) is internal/host territory and is deliberately NOT exercised
// here โ€” the loop this test proves is:
//
@@ -9,11 +10,15 @@
// 2. a shipped+promoted static tree is served through the edge,
// 3. promote advances the served content, rollback returns the pointer to
// the previous version, and the static handler swaps back on re-check.
+// 4. one fleet status publish tick lands status/{index.html,fleet.json} in
+// the same deployment bucket, and the edge's kind=status route serves
+// both back out through the identical Host-routed path.
package main_test
import (
"context"
"crypto/ed25519"
+ "encoding/json"
"fmt"
"io"
"net"
@@ -29,6 +34,7 @@ import (
"github.com/carlosframework/platform/internal/edge"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
"github.com/carlosframework/platform/internal/vet"
)
@@ -263,6 +269,9 @@ func TestEndToEndSmoke(t *testing.T) {
if err := reg.Upsert(registry.Route{Host: "site.local", Kind: "static", App: "site", Channel: "canary/x", Status: "ready"}); err != nil {
t.Fatalf("upsert site route: %v", err)
}
+ if err := reg.Upsert(registry.Route{Host: "status.local", Kind: "status", Status: "ready"}); err != nil {
+ t.Fatalf("upsert status route: %v", err)
+ }
e, err := edge.New(reg, nil)
if err != nil {
t.Fatalf("edge.New: %v", err)
@@ -318,6 +327,42 @@ func TestEndToEndSmoke(t *testing.T) {
t.Fatalf("after rollback, GET site.local / = %d %q, want 200 %q", code, body, "site v1")
}
+ // (4) The fleet status page: one publish tick, then served through the
+ // same edge by Host header. statusTick (internal/host/agent.go) is
+ // unexported and lives in a different package, so this composes the
+ // identical seam it runs on every 5-minute tick โ€” CollectBox -> WriteBox
+ // -> BuildFleet -> PublishFleet โ€” directly over the same DirStore the
+ // release plane above already writes to (EnableStatic wired that same
+ // `store` in as the status route's backing store too).
+ statusRoutes, err := reg.List()
+ if err != nil {
+ t.Fatalf("list routes for status tick: %v", err)
+ }
+ tickNow := time.Now()
+ box := status.CollectBox(statusRoutes, status.Probes{MachineID: "e2e-box"}, tickNow)
+ if err := status.WriteBox(ctx, store, box); err != nil {
+ t.Fatalf("WriteBox: %v", err)
+ }
+ fleet, err := status.BuildFleet(ctx, store, statusRoutes, tickNow, 5*time.Minute)
+ if err != nil {
+ t.Fatalf("BuildFleet: %v", err)
+ }
+ if err := status.PublishFleet(ctx, store, fleet); err != nil {
+ t.Fatalf("PublishFleet: %v", err)
+ }
+
+ if code, body := e2eGet(t, srv.URL, "status.local", "/"); code != 200 || !strings.Contains(body, "Fleet status") {
+ t.Fatalf("GET status.local / = %d %q, want 200 containing %q", code, body, "Fleet status")
+ }
+ code, body := e2eGet(t, srv.URL, "status.local", "/fleet.json")
+ if code != 200 {
+ t.Fatalf("GET status.local /fleet.json = %d, want 200", code)
+ }
+ var decodedFleet status.Fleet
+ if err := json.Unmarshal([]byte(body), &decodedFleet); err != nil {
+ t.Fatalf("decode status.local /fleet.json: %v\nbody: %s", err, body)
+ }
+
// Binary channel pointer flow: ship v2 (same bytes โ€” a rebuild), promote,
// then roll back; the pointer must return to v1 with v2 as the rollback
// target. (Applying the pointer on a box is host/agent territory.)
@@ -343,3 +388,15 @@ func TestEndToEndSmoke(t *testing.T) {
t.Fatalf("rolled-back pointer = %s (history %v), want v1 with v2 first in history", cp.Version, cp.History)
}
}
+
+// TestStatusDryRun exercises the CLI's `carlos status -dry-run <dir>` path.
+// RunStatus (cli.go) is three lines of flag parsing around status.DryRun โ€”
+// the substance under test is DryRun itself, run the way an operator would
+// run it against a scratch directory before ever pointing the binary at a
+// real bucket: build a fixture fleet stuffed with needles, publish it, read
+// every object back, and fail loudly if anything leaked.
+func TestStatusDryRun(t *testing.T) {
+ if err := status.DryRun(t.TempDir(), io.Discard); err != nil {
+ t.Fatalf("status.DryRun: %v", err)
+ }
+}