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>
3 files changed,
+140
−2
README.md+78 −0docs/superpowers/specs/2026-08-01-carlos-platform-design.md+3 −0e2e_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) |
| + } |
| +} |