carlos / platform Public

e2e: fleet boxes converge with no bucket credentials (spec proof)

Task 5 of the fleet-api plan. internal/edge/fleetapi_e2e_test.go stands up a
real edge (registry-backed TunnelHub auth, the reload-time fleet->account
snapshot, EnableStatic's own DirStore handle behind the door) and two fake
fleet boxes whose ENTIRE view of the deployment bucket is fleetapi.New over
that door — no bucket name, no AWS credentials, anywhere in the box half.

Seven legs against one world, each depending on the last: control/serial
reads and refuses the box's write; provision.Reconcile mints the fleet's own
instance route through the account-scoped listing path (and does not mint the
second tenant's, planted in this box's own region); release.Pass converges the
minted route — signed pointer, manifest, 64 KiB artifact streamed and staged;
the heartbeat lands at fleets/<name>/status/boxes/<label>.json under Task 4's
door-branch prefix default and replaces itself under CAS; a binding takeover
written through the door reaches routing via ReconcileFleetBindings and the
bound box then serves real traffic on its channel; the second tenant is
invisible to list, dirs, get, put and delete; log chunks ship, list and prune
through the Task 4 grant while neighbouring keys stay refused.

Mutation-checked, not merely green: making aclRead permissive fails the
tenancy leg, and permissive aclRead + an unmapped apps/ list scope mints the
other account's route on the box — the failure the leg exists to catch.

Fold-ins: spec F3 now states that the logs grant is account-wide (a
compromised fleet box can forge or delete chunks for the account's apps on
PLATFORM boxes too, with no server-side quota beyond the 32 MiB per-PUT cap;
a door-side chunk shape/size guard is a filed follow-up), and Task 2's
deferred pins land — byte-identical refusal rows for another account's
manifest and three /store/dirs classes, plus accounts/<own-sqid>/ pinned as a
DELIBERATE list-scope refusal with the aclDir-stricter-than-aclRead trap named.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell b0e2e54f4356fec8dbc8542c172d021029440e97 parent a5c6cc3
3 files changed, +571 −1
  • docs/superpowers/specs/2026-08-11-fleet-api-surface-design.md +1 −1
  • internal/edge/fleetapi_e2e_test.go +544 −0
  • internal/edge/storedoor_test.go +26 −0
diff --git a/docs/superpowers/specs/2026-08-11-fleet-api-surface-design.md b/docs/superpowers/specs/2026-08-11-fleet-api-surface-design.md
index b6bc091eb..f4d583723 100644
--- a/docs/superpowers/specs/2026-08-11-fleet-api-surface-design.md
+++ b/docs/superpowers/specs/2026-08-11-fleet-api-surface-design.md
@@ -49,7 +49,7 @@ reconcile, config, status, metrics, bindings — runs unchanged.
|---|---|---|
| F1 | **Transport is the edge's reserved tunnel hostname**, new paths beside `/register`: `GET/PUT/DELETE /store/o/<key>`, `GET /store/list?prefix=`, `GET /store/dirs?prefix=`. Same headers as the tunnel dial: `Authorization: Bearer <token>` + `Carlos-Tunnel-Name: <fleet>/<label>`. | The reserved host is already TLS'd, already routed, already carries the only box-shaped credential the platform can verify. No new hostname, cert, or credential mint. |
| F2 | **Auth = the dynamic-auth table**, via a new `TunnelHub.VerifyDynamic(name, token) (fleet string, ok bool)` refactored from ServeHTTP's dynamic branch (constant-time hash compare, single-401 shape preserved; static `CARLOS_TUNNEL_CLIENTS` names never authenticate here — this door is for fleet boxes only). | One credential, one hash table, one revocation path: detach/rotate already converge to the edge in seconds and now revoke API access with the same motion. |
-| F3 | **ACL is account-scoped + own-prefix**, derived server-side from the authenticated fleet's registry row (`fleets.account` → sqid): READ allows `control/serial`, `bootstrap/carlos`, `accounts/<own-sqid>/**` (the account manifest is `accounts/<sqid>/account.json` — `domains.AccountManifestKey`, one path level deep — NOT `accounts/<sqid>.json`; a box that cannot read it sees zero accounts and `provision.Reconcile` retires every provisioned route on it), and `apps/<own-sqid>/**`; list of `accounts/` and `apps/` answers only the own-account subset, INCLUDING the delimiter-aware listing — `ListDirs("accounts/")` returns one child prefix per account on the platform, so it is filtered per tenant, not against the prefix it was listed under. WRITE/DELETE allow `fleets/<own-fleet>/**` and `apps/<own-sqid>/<one app segment>/logs/**` (the app-log chunks `logshipLoop` writes and prunes — `applog.Prefix`), and nothing else. Everything else: byte-identical 404 (never 403 — the keyspace must not be an oracle).<br><br>The logs grant is **segment-exact**, and the exactness is the security property: `apps/<sqid>/<app>/logs/**`, never `apps/<sqid>/**` and never a "the key contains `logs/`" test. A blanket own-account write would let a box pin its own `channels/*.json` (self-promoting any build it likes) or mint `instances/*.json` records (self-authorizing routes) — the exact writes this door exists to prevent.<br><br>**Caveat (accepted):** the `<box>` segment inside an app's log subtree is an unverifiable pseudonym (`status.Label` of a machine id the box chose), so one box of an account can delete a same-account sibling box's chunks. Blast radius is inside a single account's own logs — no other tenant, no release data — and pruning its own subtree is what logship must do hourly. Optional hardening if it ever matters: bind the box segment to the authenticated channel label, which needs a per-box identity the door does not carry today.<br><br>**Deliberately NOT granted:** provisioning record writes (`apps/<sqid>/<app>/instances/*.json`). A box's `provision.Reconcile` makes zero store writes — every `Put`/`Delete` on those keys is console- or CLI-side — so there is nothing to grant, and granting it would hand a box self-authorization over its own routes. | This is the issue's "account-scoped surface" literally: a fleet reads only its owning account's estate, and writes only its own corner plus the log chunks it produces about its own tenant's apps. A compromised customer box can no longer read other tenants or touch platform releases. |
+| F3 | **ACL is account-scoped + own-prefix**, derived server-side from the authenticated fleet's registry row (`fleets.account` → sqid): READ allows `control/serial`, `bootstrap/carlos`, `accounts/<own-sqid>/**` (the account manifest is `accounts/<sqid>/account.json` — `domains.AccountManifestKey`, one path level deep — NOT `accounts/<sqid>.json`; a box that cannot read it sees zero accounts and `provision.Reconcile` retires every provisioned route on it), and `apps/<own-sqid>/**`; list of `accounts/` and `apps/` answers only the own-account subset, INCLUDING the delimiter-aware listing — `ListDirs("accounts/")` returns one child prefix per account on the platform, so it is filtered per tenant, not against the prefix it was listed under. WRITE/DELETE allow `fleets/<own-fleet>/**` and `apps/<own-sqid>/<one app segment>/logs/**` (the app-log chunks `logshipLoop` writes and prunes — `applog.Prefix`), and nothing else. Everything else: byte-identical 404 (never 403 — the keyspace must not be an oracle).<br><br>The logs grant is **segment-exact**, and the exactness is the security property: `apps/<sqid>/<app>/logs/**`, never `apps/<sqid>/**` and never a "the key contains `logs/`" test. A blanket own-account write would let a box pin its own `channels/*.json` (self-promoting any build it likes) or mint `instances/*.json` records (self-authorizing routes) — the exact writes this door exists to prevent.<br><br>**Caveat (accepted):** the `<box>` segment inside an app's log subtree is an unverifiable pseudonym (`status.Label` of a machine id the box chose), so one box of an account can delete a same-account sibling box's chunks. Blast radius is inside a single account's own logs — no other tenant, no release data — and pruning its own subtree is what logship must do hourly. Optional hardening if it ever matters: bind the box segment to the authenticated channel label, which needs a per-box identity the door does not carry today. Wider than the sibling-box case, and accepted on the same terms: the logs grant is **account-wide**, so a compromised fleet box can forge or delete log chunks for *any* of its account's apps — including apps that run on the account's PLATFORM boxes and never touch the fleet at all — and there is no server-side quota on what it writes beyond the 32 MiB per-PUT cap; a door-side chunk shape/size guard is a filed follow-up, not part of v1.<br><br>**Deliberately NOT granted:** provisioning record writes (`apps/<sqid>/<app>/instances/*.json`). A box's `provision.Reconcile` makes zero store writes — every `Put`/`Delete` on those keys is console- or CLI-side — so there is nothing to grant, and granting it would hand a box self-authorization over its own routes. | This is the issue's "account-scoped surface" literally: a fleet reads only its owning account's estate, and writes only its own corner plus the log chunks it produces about its own tenant's apps. A compromised customer box can no longer read other tenants or touch platform releases. |
| F4 | **CAS passes through**: Get returns the backing etag in `ETag`; Put requires `If-Match` (or `If-None-Match: *` for create), maps conflict to 412; the client maps 412 back to `release.ErrCASConflict`. Deletes are idempotent 204. | The lease-adjacent writers (heartbeat CAS, binding takeover guard) keep their exact semantics through the proxy. |
| F5 | **Client = `internal/fleetapi.Store`**, implementing `release.Store` over the F1 wire with the box's existing `CARLOS_TUNNEL_URL`/`_NAME`/`_TOKEN` env. Selection: `openDeployStore` (and the status/metrics bucket resolution) returns the API store **when fleet identity resolves AND `CARLOS_DEPLOYMENT_BUCKET`/`CARLOS_STATUS_BUCKET` is unset**; a bucket still set wins (the issue's migration fallback — platform boxes and grandfathered fleet boxes keep direct S3, byte-for-byte unchanged). Log shipping arms off the same selection (so `carlos logs` keeps working on a bucketless box, with keys left UNPREFIXED — the console reads the platform layout). On the door branch only, an empty `CARLOS_STATUS_PREFIX` defaults to `fleets/<fleet>/`: status/metrics at the shared root are outside the door's write ACL and would 404 forever. | Env-driven cutover, box by box, no flag day: drop the bucket var + AWS creds from a box's env and it is on the API; put them back and it is off. |
| F6 | **retentionLoop does not arm on a fleet box** (fleet identity present → skip with one log line). | A customer box scheduling deletes of platform-bucket releases was wrong under direct S3 too; the ACL would refuse it anyway — skipping avoids an hourly error loop. Platform boxes keep retention. |
diff --git a/internal/edge/fleetapi_e2e_test.go b/internal/edge/fleetapi_e2e_test.go
new file mode 100644
index 000000000..c32c34101
--- /dev/null
+++ b/internal/edge/fleetapi_e2e_test.go
@@ -0,0 +1,544 @@
+// Task 5 of the fleet-api plan: the spec's central promise proved end to end
+// — a fleet box holding NO bucket credentials converges entirely through the
+// store door (spec 2026-08-11-fleet-api-surface-design.md, F1-F8; issue #73).
+//
+// The box's whole view of the platform's deployment bucket is a
+// fleetapi.New(...) client pointed at a REAL edge: real TunnelHub auth
+// (registry-backed, the same table detach/rotate converge), the real
+// fleet→account snapshot Reload builds, the real ACL, a DirStore behind it.
+// No box-side code path in this file ever touches that DirStore — every
+// platform-side use of it below is the OTHER role (the console/CLI writing
+// the bucket, or the assertion reading back what a box wrote through the
+// door).
+//
+// It sits beside fleet_e2e_test.go rather than inside it because the two
+// prove different things about the same fleet and want different worlds:
+// TestTwoBoxFleetEndToEnd drives the ROUTING failure sequence (channels,
+// leases, takeover, dark fleet) and its edge deliberately has no static
+// store — no door at all. This one drives the STORE loop (adopt, serial,
+// provision, heartbeat, bindings, logs) and needs EnableStatic wired. What
+// they do share is shared: e2eHashToken, startFleetBox, namedFleetHandler,
+// waitChannel, e2eGet and waitBody all come from that file, so the edge/hub
+// arrangement has exactly one source of truth per concern.
+//
+// Hermetic and fast: loopback httptest edge, real h2 reverse channels via
+// the production tunnelclient.Loop, real ed25519 release signing.
+package edge_test
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "errors"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/applog"
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/edge"
+ "github.com/carlosframework/platform/internal/fleetapi"
+ "github.com/carlosframework/platform/internal/host"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
+)
+
+const (
+ fapiFleet = "orchard"
+ fapiBoxA = "box-a"
+ fapiBoxB = "box-b"
+ fapiTokenA = "orchard-token-a"
+ fapiTokenB = "orchard-token-b"
+
+ fapiApp = "shop"
+ fapiDomain = "orchard.example"
+ fapiHost = "acme." + fapiDomain
+ fapiRegion = "ie"
+ fapiChannel = "canary/e2e" // canary/* has no bake window: the pass applies at once
+ fapiVersion = "v1"
+
+ // The two account identities. Both are real sqids (domains.Sqid of an id),
+ // never literals: provision.knownAccounts refuses any account manifest
+ // whose path sqid, body sqid and Sqid(body.ID) do not all agree, so a
+ // hardcoded string would quietly stop being an account the moment the
+ // frozen encoding changed — and this test's whole provisioning leg would
+ // then pass for the wrong reason (nothing known, nothing minted).
+ fapiAcctID int64 = 7
+ fapiOtherID int64 = 8
+ fapiOtherApp = "intruder"
+)
+
+// fapiPlatform is the artifact key a box of THIS machine's shape adopts —
+// release's own adoptPlatform, which is unexported.
+var fapiPlatform = runtime.GOOS + "-" + runtime.GOARCH
+
+// fleetDoorWorld is one platform + one fleet: the edge and its bucket on one
+// side, two fleet boxes whose only bucket access is the door on the other.
+type fleetDoorWorld struct {
+ reg *registry.Registry // the PLATFORM's registry: fleet rows, box credentials, fleet-placed routes
+ store *release.DirStore // the platform's deployment bucket — edge-side and console-side handle
+ srvURL string
+ hub *edge.TunnelHub
+ priv ed25519.PrivateKey
+ pub ed25519.PublicKey
+ sqid string // the fleet's owning account
+ other string // a second tenant on the same bucket, which the fleet must never see
+ boxA release.Store
+ boxB release.Store
+}
+
+// newFleetDoorWorld stands the world up and returns it with both boxes'
+// channels connected — the same credential that dials the tunnel is the one
+// their store clients authenticate with, which is F1/F2 stated as a fixture.
+func newFleetDoorWorld(t *testing.T) *fleetDoorWorld {
+ t.Helper()
+ w := &fleetDoorWorld{
+ sqid: domains.Sqid(fapiAcctID),
+ other: domains.Sqid(fapiOtherID),
+ }
+
+ // Not closed at cleanup, for the reason fleet_e2e_test.go gives: Edge.Watch
+ // may still be mid-poll when the test returns.
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatalf("open registry: %v", err)
+ }
+ w.reg = reg
+ if err := reg.UpsertFleet(registry.Fleet{Name: fapiFleet, Account: w.sqid, Bucket: "orchard-data"}); err != nil {
+ t.Fatalf("UpsertFleet: %v", err)
+ }
+ for label, tok := range map[string]string{fapiBoxA: fapiTokenA, fapiBoxB: fapiTokenB} {
+ if err := reg.AddFleetBox(registry.FleetBox{
+ Fleet: fapiFleet, Label: label, TokenHash: e2eHashToken(tok), Status: "active",
+ }); err != nil {
+ t.Fatalf("AddFleetBox(%s): %v", label, err)
+ }
+ }
+
+ w.store = release.NewDirStore(t.TempDir())
+ seedB64, pubB64, err := release.GenerateKey()
+ if err != nil {
+ t.Fatalf("GenerateKey: %v", err)
+ }
+ if w.priv, err = release.ParseSigningKey(seedB64, "test"); err != nil {
+ t.Fatalf("ParseSigningKey: %v", err)
+ }
+ if w.pub, err = release.ParsePublicKey(pubB64, "test"); err != nil {
+ t.Fatalf("ParsePublicKey: %v", err)
+ }
+
+ e, err := edge.New(reg, nil)
+ if err != nil {
+ t.Fatalf("edge.New: %v", err)
+ }
+ srv := httptest.NewServer(e)
+ t.Cleanup(srv.Close)
+ w.srvURL = srv.URL
+ u, err := url.Parse(srv.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // EnableStatic is what hands the door its store handle — the edge's OWN,
+ // never a second client, and the door is only reachable on the hub's
+ // reserved hostname (here the listener's own, as in production
+ // CARLOS_TUNNEL_HOST).
+ w.hub = edge.NewTunnelHub(u.Hostname(), nil)
+ e.EnableStatic(w.store, w.pub, filepath.Join(t.TempDir(), "cache"))
+ e.EnableTunnels(w.hub)
+ if err := e.Reload(); err != nil {
+ t.Fatalf("Reload: %v", err)
+ }
+ stopWatch := make(chan struct{})
+ t.Cleanup(func() { close(stopWatch) })
+ go e.Watch(stopWatch)
+
+ stopA := startFleetBox(t, srv.URL, fapiFleet+"/"+fapiBoxA, fapiTokenA, namedFleetHandler(fapiBoxA))
+ t.Cleanup(stopA)
+ stopB := startFleetBox(t, srv.URL, fapiFleet+"/"+fapiBoxB, fapiTokenB, namedFleetHandler(fapiBoxB))
+ t.Cleanup(stopB)
+ waitChannel(t, w.hub, fapiFleet+"/"+fapiBoxA, "connected", e2eDialBound)
+ waitChannel(t, w.hub, fapiFleet+"/"+fapiBoxB, "connected", e2eDialBound)
+
+ // The boxes' deployment stores. srv.URL is the door's base exactly as
+ // CARLOS_TUNNEL_URL is in production, and the name is the box's own
+ // channel identity — no AWS config, no bucket name, no credentials.
+ w.boxA = fapiStore(t, srv.URL, fapiBoxA, fapiTokenA)
+ w.boxB = fapiStore(t, srv.URL, fapiBoxB, fapiTokenB)
+ return w
+}
+
+func fapiStore(t *testing.T, base, label, token string) release.Store {
+ t.Helper()
+ s, err := fleetapi.New(base, fapiFleet+"/"+label, token)
+ if err != nil {
+ t.Fatalf("fleetapi.New(%s): %v", label, err)
+ }
+ return s
+}
+
+// put is a platform-side (console/CLI-shaped) write straight to the bucket:
+// create-only, the same shape every real writer of these keys uses.
+func (w *fleetDoorWorld) put(t *testing.T, key, body string) {
+ t.Helper()
+ if _, err := w.store.Put(context.Background(), key, []byte(body), ""); err != nil {
+ t.Fatalf("seed %s: %v", key, err)
+ }
+}
+
+// accountManifest is domains' account record at the key provision reads —
+// accounts/<sqid>/account.json, one level deep (never accounts/<sqid>.json).
+func fapiAccountManifest(id int64, sqid, name string) string {
+ b, _ := json.Marshal(map[string]any{
+ "id": id, "sqid": sqid, "name": name, "created_at": "2026-08-11T09:00:00Z",
+ })
+ return string(b)
+}
+
+// TestFleetBoxConvergesWithoutBucketCredentials is Task 5's acceptance
+// evidence. The legs run in sequence against ONE world, each depending on
+// what the last left behind (a record becomes a route, the route adopts a
+// release, the box heartbeats about it, its binding lands in routing) — a
+// failure means the loop stopped there and nothing after it is meaningful.
+func TestFleetBoxConvergesWithoutBucketCredentials(t *testing.T) {
+ ctx := context.Background()
+ w := newFleetDoorWorld(t)
+
+ // A second tenant on the same bucket, planted BEFORE anything else so
+ // every listing below runs against a store that really does hold another
+ // account's keys — the negative leg (f) is only meaningful if the data it
+ // says is invisible is genuinely there.
+ w.put(t, "accounts/"+w.other+"/account.json", fapiAccountManifest(fapiOtherID, w.other, "Intruder"))
+ otherChannelKey := "apps/" + w.other + "/" + fapiOtherApp + "/channels/stable.json"
+ w.put(t, otherChannelKey, `{"app":"intruder","version":"v9"}`)
+ otherLogKey := applog.ChunkKey(w.other, fapiOtherApp, fapiBoxA, applog.HourOf(time.Now()), 1, 9)
+ w.put(t, otherLogKey, "not yours")
+ w.put(t, "fleets/other/status/boxes/box-a.json", `{"label":"box-a"}`)
+
+ // --- (a) the 2s convergence poll: control/serial, read-only --------------
+ // The one shared platform key a box polls every 2 seconds. It must read,
+ // and it must never be writable — a box that could bump the serial could
+ // make every other box in the deployment run a full pass set on demand.
+ w.put(t, release.SerialKey, "41")
+ serial, _, err := w.boxA.Get(ctx, release.SerialKey)
+ if err != nil || string(serial) != "41" {
+ t.Fatalf("(a) serial through the door = %q, %v; want \"41\"", serial, err)
+ }
+ _, etag, err := w.store.Get(ctx, release.SerialKey)
+ if err != nil {
+ t.Fatalf("(a) read serial etag: %v", err)
+ }
+ if _, err := w.store.Put(ctx, release.SerialKey, []byte("42"), etag); err != nil {
+ t.Fatalf("(a) console bump: %v", err)
+ }
+ if serial, _, err = w.boxA.Get(ctx, release.SerialKey); err != nil || string(serial) != "42" {
+ t.Fatalf("(a) serial after the console bump = %q, %v; want \"42\" — the poll would never see a deploy", serial, err)
+ }
+ if _, err := w.boxA.Put(ctx, release.SerialKey, []byte("999"), ""); err == nil {
+ t.Fatal("(a) a fleet box bumped control/serial through the door")
+ }
+ t.Log("(a) control/serial reads through the door and refuses the box's write")
+
+ // --- (b) provisioning: an instance record becomes a local route ----------
+ // The account-scoped LISTING path end to end: knownAccounts lists
+ // accounts/ (a shared prefix, filtered per tenant), the record scan lists
+ // apps/ (scope-mapped to this account's subtree), and the pass mints.
+ w.put(t, "accounts/"+w.sqid+"/account.json", fapiAccountManifest(fapiAcctID, w.sqid, "Orchard"))
+ if err := provision.PutManifest(ctx, w.store, w.sqid, fapiApp, provision.Manifest{
+ Schema: 1, App: fapiApp, Domain: fapiDomain, Regions: []string{fapiRegion}, Seed: []string{"serve"},
+ }); err != nil {
+ t.Fatalf("(b) PutManifest: %v", err)
+ }
+ if err := provision.CreateRecord(ctx, w.store, provision.Record{
+ Schema: 1, Host: fapiHost, Slug: "acme", App: fapiApp, Account: w.sqid,
+ Region: fapiRegion, Channel: fapiChannel, CreatedAt: time.Now(),
+ }); err != nil {
+ t.Fatalf("(b) CreateRecord: %v", err)
+ }
+ // The other tenant's record, in THIS box's region and pool: the only thing
+ // keeping it off this box is the door's account scope.
+ if err := provision.CreateRecord(ctx, w.store, provision.Record{
+ Schema: 1, Host: "evil." + fapiDomain, Slug: "evil", App: fapiOtherApp, Account: w.other,
+ Region: fapiRegion, Channel: "stable", CreatedAt: time.Now(),
+ }); err != nil {
+ t.Fatalf("(b) CreateRecord (other tenant): %v", err)
+ }
+
+ // The box's OWN registry — a fleet box has one, separate from the
+ // platform's, and this pass is what puts routes in it.
+ boxReg, err := registry.Open(filepath.Join(t.TempDir(), "box-registry.db"))
+ if err != nil {
+ t.Fatalf("(b) open box registry: %v", err)
+ }
+ dataDir := t.TempDir()
+ if err := provision.Reconcile(ctx, w.boxA, boxReg, provision.ReconcileConfig{
+ Region: fapiRegion, DataDir: dataDir,
+ }); err != nil {
+ t.Fatalf("(b) provision.Reconcile through the door: %v", err)
+ }
+ minted, err := boxReg.Get(fapiHost)
+ if err != nil || minted == nil {
+ t.Fatalf("(b) route %s = %+v, %v — the record never became a route", fapiHost, minted, err)
+ }
+ if minted.App != fapiApp || minted.Account != w.sqid || minted.Channel != fapiChannel ||
+ !minted.Provisioned || minted.Seed != `["serve"]` {
+ t.Fatalf("(b) minted route = %+v, want the record's app/account/channel, Provisioned and the manifest seed", minted)
+ }
+ if rt, err := boxReg.Get("evil." + fapiDomain); err != nil || rt != nil {
+ t.Fatalf("(b) the other tenant's record minted a route on this box: %+v, %v", rt, err)
+ }
+ t.Log("(b) provision.Reconcile minted the fleet's own instance route and could not see the other tenant's")
+
+ // --- (c) release adoption: pointer, artifact, staging --------------------
+ // The whole adoption pass over the door: the signed channel pointer is
+ // fetched and verified, the manifest read, the artifact BODY streamed
+ // through the edge (F8 — no presigned redirect), and the version staged
+ // and stamped on the route.
+ artifact := make([]byte, 64<<10)
+ for i := range artifact {
+ artifact[i] = byte(i * 7)
+ }
+ if err := release.Ship(ctx, w.store, w.sqid, release.Manifest{
+ App: fapiApp, Version: fapiVersion, Kind: "binary",
+ Artifacts: map[string]release.Artifact{fapiPlatform: {}},
+ }, map[string][]byte{fapiPlatform: artifact}); err != nil {
+ t.Fatalf("(c) Ship: %v", err)
+ }
+ if err := release.Promote(ctx, w.store, w.priv, w.sqid, fapiApp, fapiChannel, fapiVersion); err != nil {
+ t.Fatalf("(c) Promote: %v", err)
+ }
+ optDir := t.TempDir()
+ var restarted []string
+ restart := func(h string) error { restarted = append(restarted, h); return nil }
+ if err := release.Pass(ctx, boxReg, w.boxA, w.pub, optDir, restart); err != nil {
+ t.Fatalf("(c) release.Pass through the door: %v", err)
+ }
+ if v, err := release.LiveVersion(optDir, fapiHost); v != fapiVersion {
+ t.Fatalf("(c) live version = %q (%v), want %q — adoption did not converge through the door", v, err, fapiVersion)
+ }
+ staged, err := os.ReadFile(release.VersionPath(optDir, w.sqid, fapiVersion, fapiApp))
+ if err != nil || !slices.Equal(staged, artifact) {
+ t.Fatalf("(c) staged artifact = %d bytes (%v), want the shipped %d — a truncated body would pass a shorter check", len(staged), err, len(artifact))
+ }
+ adopted, err := boxReg.Get(fapiHost)
+ if err != nil || adopted == nil || adopted.Version != fapiVersion {
+ t.Fatalf("(c) route version = %+v, %v; want %q stamped", adopted, err, fapiVersion)
+ }
+ t.Logf("(c) adoption converged: pointer verified, %d artifact bytes streamed and staged (restarted %v)", len(artifact), restarted)
+
+ // --- (d) the heartbeat, under the Task-4 default prefix ------------------
+ // A door-backed box gets CARLOS_STATUS_PREFIX defaulted to
+ // fleets/<fleet>/ (spec F5) precisely because the shared status/ root is
+ // outside the door's write ACL. The two lines statusLoop runs, with that
+ // rebase and the agent's label override:
+ prefixed := release.NewPrefixStore(w.boxA, "fleets/"+fapiFleet+"/")
+ routes, err := boxReg.List()
+ if err != nil {
+ t.Fatalf("(d) list box routes: %v", err)
+ }
+ box := status.CollectBox(routes, status.Probes{MachineID: "orchard-a", Region: "IE"}, time.Now())
+ box.Label = fapiBoxA // statusTick's labelOverride: the fleet label, not a machine-id hash
+ if err := status.WriteBox(ctx, prefixed, box); err != nil {
+ t.Fatalf("(d) status.WriteBox through the door: %v", err)
+ }
+ heartbeatKey := "fleets/" + fapiFleet + "/status/boxes/" + fapiBoxA + ".json"
+ raw, _, err := w.store.Get(ctx, heartbeatKey)
+ if err != nil {
+ t.Fatalf("(d) heartbeat did not land at %s: %v", heartbeatKey, err)
+ }
+ var landed status.Box
+ if err := json.Unmarshal(raw, &landed); err != nil {
+ t.Fatalf("(d) heartbeat body: %v", err)
+ }
+ if landed.Label != fapiBoxA {
+ t.Fatalf("(d) heartbeat label = %q, want %q", landed.Label, fapiBoxA)
+ }
+ // The SECOND write is the one that exercises CAS through the door:
+ // WriteBox reads its own object's etag first (the read half of the fleet
+ // corner, which only exists so this replace can happen) and Puts with
+ // If-Match. A door that dropped etags would fail here, not above.
+ if err := status.WriteBox(ctx, prefixed, box); err != nil {
+ t.Fatalf("(d) heartbeat replace (read-etag-then-Put) through the door: %v", err)
+ }
+ // And a box may not heartbeat into a fleet that is not its own.
+ if _, err := w.boxA.Put(ctx, "fleets/other/status/boxes/box-a.json", []byte("{}"), ""); err == nil {
+ t.Fatal("(d) box-a wrote into another fleet's corner")
+ }
+ t.Logf("(d) heartbeat landed at %s and replaced itself under CAS", heartbeatKey)
+
+ // --- (e) a fleet binding takeover, box → bucket → routing ----------------
+ // The platform's route for the app, placed on the fleet and currently
+ // bound to box-b.
+ if err := w.reg.Upsert(registry.Route{
+ Host: fapiHost, Kind: "instance", Status: "ready",
+ Region: registry.PlacementTarget(fapiFleet), FleetBox: fapiBoxB,
+ }); err != nil {
+ t.Fatalf("(e) upsert fleet-placed route: %v", err)
+ }
+ // box-b's agent-startup resident pass (takeover=false), through ITS door
+ // client — an observation, with no lease behind it.
+ if err := host.WriteFleetBinding(ctx, w.boxB, host.FleetBinding{
+ Host: fapiHost, Fleet: fapiFleet, Box: fapiBoxB, BoundAt: time.Now().UnixMilli(),
+ }, false); err != nil {
+ t.Fatalf("(e) box-b resident binding: %v", err)
+ }
+ // box-a wakes the app and wins the lease (modeled in fleet_e2e_test.go;
+ // what matters here is the WRITE): a takeover claims the binding.
+ if err := host.WriteFleetBinding(ctx, w.boxA, host.FleetBinding{
+ Host: fapiHost, Fleet: fapiFleet, Box: fapiBoxA, BoundAt: time.Now().UnixMilli(),
+ }, true); err != nil {
+ t.Fatalf("(e) box-a takeover binding through the door: %v", err)
+ }
+ // The platform reads it back from its own bucket handle — the object IS
+ // the message — and restamps routing.
+ if err := host.ReconcileFleetBindings(ctx, w.store, w.reg); err != nil {
+ t.Fatalf("(e) ReconcileFleetBindings: %v", err)
+ }
+ rt, err := w.reg.Get(fapiHost)
+ if err != nil || rt == nil || rt.FleetBox != fapiBoxA {
+ t.Fatalf("(e) Route.FleetBox = %+v, %v; want %q — the takeover never made it back into routing", rt, err, fapiBoxA)
+ }
+ // And the guard survives the round trip: box-b's next resident pass reads
+ // the existing binding through the door and declines to steal it back.
+ if err := host.WriteFleetBinding(ctx, w.boxB, host.FleetBinding{
+ Host: fapiHost, Fleet: fapiFleet, Box: fapiBoxB, BoundAt: time.Now().UnixMilli(),
+ }, false); err != nil {
+ t.Fatalf("(e) box-b second resident pass: %v", err)
+ }
+ if err := host.ReconcileFleetBindings(ctx, w.store, w.reg); err != nil {
+ t.Fatalf("(e) ReconcileFleetBindings (after box-b's pass): %v", err)
+ }
+ if rt, err = w.reg.Get(fapiHost); err != nil || rt == nil || rt.FleetBox != fapiBoxA {
+ t.Fatalf("(e) Route.FleetBox = %+v, %v; want %q — a non-takeover write stole the binding through the door", rt, err, fapiBoxA)
+ }
+ // End to end, from outside: the edge's 2s poll picks the restamp up and
+ // the app is served by box-a's own channel. Record → route → release →
+ // binding → traffic, with the box holding no bucket credential anywhere
+ // in that chain.
+ waitBody(t, w.srvURL, fapiHost, fapiBoxA, e2eConvergeBound)
+ t.Log("(e) the takeover written through the door reached routing, and the bound box serves")
+
+ // --- (f) the other tenant is invisible ----------------------------------
+ // Listings first: the easiest place to leak a neighbour, and the one
+ // provision.Reconcile above depends on being right.
+ appKeys, err := w.boxA.List(ctx, "apps/")
+ if err != nil {
+ t.Fatalf("(f) list apps/: %v", err)
+ }
+ for _, k := range appKeys {
+ if !strings.HasPrefix(k, "apps/"+w.sqid+"/") {
+ t.Fatalf("(f) list apps/ returned %q — outside this fleet's account", k)
+ }
+ }
+ if len(appKeys) == 0 {
+ t.Fatal("(f) list apps/ came back empty — the scope map is refusing the fleet's OWN estate")
+ }
+ acctKeys, err := w.boxA.List(ctx, "accounts/")
+ if err != nil {
+ t.Fatalf("(f) list accounts/: %v", err)
+ }
+ if !slices.Equal(acctKeys, []string{"accounts/" + w.sqid + "/account.json"}) {
+ t.Fatalf("(f) list accounts/ = %v, want only this fleet's own account record", acctKeys)
+ }
+ acctDirs, err := w.boxA.ListDirs(ctx, "accounts/")
+ if err != nil {
+ t.Fatalf("(f) listdirs accounts/: %v", err)
+ }
+ if !slices.Equal(acctDirs, []string{"accounts/" + w.sqid + "/"}) {
+ t.Fatalf("(f) listdirs accounts/ = %v, want only this fleet's own directory (one child per account exists)", acctDirs)
+ }
+ fleetDirs, err := w.boxA.ListDirs(ctx, "fleets/")
+ if err != nil {
+ t.Fatalf("(f) listdirs fleets/: %v", err)
+ }
+ for _, d := range fleetDirs {
+ if !strings.HasPrefix(d, "fleets/"+fapiFleet+"/") {
+ t.Fatalf("(f) listdirs fleets/ returned %q — another fleet's corner", d)
+ }
+ }
+ // Then direct gets: every one of these keys exists in the bucket, and
+ // every one of them is os.ErrNotExist to this box.
+ for _, key := range []string{
+ "accounts/" + w.other + "/account.json",
+ otherChannelKey,
+ otherLogKey,
+ "apps/" + w.other + "/" + fapiOtherApp + "/instances/evil." + fapiDomain + ".json",
+ "fleets/other/status/boxes/box-a.json",
+ } {
+ if _, _, err := w.store.Get(ctx, key); err != nil {
+ t.Fatalf("(f) fixture %s is not actually in the bucket: %v", key, err)
+ }
+ if _, _, err := w.boxA.Get(ctx, key); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("(f) GET %s through the door = %v, want os.ErrNotExist (byte-identical 404)", key, err)
+ }
+ }
+ // Writes and deletes into the neighbour are refused too — including the
+ // delete of a log chunk, which is the one write verb the account-wide logs
+ // grant makes tempting.
+ if _, err := w.boxA.Put(ctx, otherChannelKey, []byte(`{"version":"evil"}`), ""); err == nil {
+ t.Fatal("(f) box-a promoted a release in another account")
+ }
+ if _, err := w.boxA.Put(ctx, otherLogKey, []byte("forged"), ""); err == nil {
+ t.Fatal("(f) box-a forged a log chunk in another account")
+ }
+ if err := w.boxA.Delete(ctx, otherLogKey); err == nil {
+ t.Fatal("(f) box-a deleted another account's log chunk")
+ }
+ if _, _, err := w.store.Get(ctx, otherChannelKey); err != nil {
+ t.Fatalf("(f) the refused writes changed the neighbour's object anyway: %v", err)
+ }
+ t.Log("(f) the second tenant is invisible to this fleet: listings, gets, writes and deletes all refuse")
+
+ // --- (g) log chunks: ship and prune through the door ---------------------
+ // The Task-4 grant, exercised as logshipLoop uses it: chunks are written
+ // UNPREFIXED under the app's own subtree (applog.Prefix — where `carlos
+ // logs` reads them), listed back, and pruned by the same box.
+ hour := applog.HourOf(time.Now())
+ chunk, err := applog.EncodeChunk([]applog.Record{{
+ TS: time.Now(), Box: fapiBoxA, Host: fapiHost, Stream: "stdout", Msg: "listening", Seq: 1,
+ }})
+ if err != nil {
+ t.Fatalf("(g) EncodeChunk: %v", err)
+ }
+ chunkKey := applog.ChunkKey(w.sqid, fapiApp, fapiBoxA, hour, 1, len(chunk))
+ if _, err := w.boxA.Put(ctx, chunkKey, chunk, ""); err != nil {
+ t.Fatalf("(g) ship a log chunk through the door: %v", err)
+ }
+ landedChunk, _, err := w.store.Get(ctx, chunkKey)
+ if err != nil {
+ t.Fatalf("(g) chunk did not land at %s: %v", chunkKey, err)
+ }
+ recs, err := applog.DecodeChunk(landedChunk)
+ if err != nil || len(recs) != 1 || recs[0].Msg != "listening" {
+ t.Fatalf("(g) chunk round trip = %+v, %v", recs, err)
+ }
+ listed, err := w.boxA.List(ctx, applog.BoxPrefix(w.sqid, fapiApp, fapiBoxA))
+ if err != nil || !slices.Contains(listed, chunkKey) {
+ t.Fatalf("(g) list own log subtree = %v, %v; want %s", listed, err, chunkKey)
+ }
+ if err := w.boxA.Delete(ctx, chunkKey); err != nil {
+ t.Fatalf("(g) prune own chunk through the door: %v", err)
+ }
+ if _, _, err := w.store.Get(ctx, chunkKey); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("(g) chunk survived its prune: %v", err)
+ }
+ // The grant stays segment-exact: the same account, the same app, one
+ // segment over, is still refused.
+ for _, key := range []string{
+ release.ChannelKey(w.sqid, fapiApp, fapiChannel),
+ provision.RecordKey(w.sqid, fapiApp, fapiHost),
+ "apps/" + w.sqid + "/" + fapiApp + "/config/production.json",
+ } {
+ if _, err := w.boxA.Put(ctx, key, []byte("{}"), ""); err == nil {
+ t.Fatalf("(g) the logs grant let the box write %s", key)
+ }
+ }
+ t.Log("(g) log chunks ship, list and prune through the door; neighbouring keys stay refused")
+}
diff --git a/internal/edge/storedoor_test.go b/internal/edge/storedoor_test.go
index 34f5abb05..a2eb8011d 100644
--- a/internal/edge/storedoor_test.go
+++ b/internal/edge/storedoor_test.go
@@ -218,6 +218,19 @@ func TestACLListScope(t *testing.T) {
{"accounts/", "accounts/", true},
{"accounts", "accounts/", true},
{"accounts/zz99.json", "", false},
+ // DELIBERATE, and the trap worth naming: aclDir is STRICTER than
+ // aclRead here. aclRead admits accounts/<own-sqid>/** as an object
+ // read, but the listing scope map admits only the bare, SHARED
+ // accounts/ prefix — a caller narrowing its listing to its own
+ // account directory is refused. That is not an oversight to "fix":
+ // every real caller of this prefix lists accounts/ whole
+ // (provision.knownAccounts, domains.Reconcile, console.materialize)
+ // and takes the per-tenant filter serveListing applies, so a
+ // per-tenant mapping row here would widen the scope map for no
+ // caller. If one ever appears, adding the row is the deliberate act
+ // this pin makes visible.
+ {"accounts/" + testSqid + "/", "", false},
+ {"accounts/" + testSqid, "", false},
{"fleets/", "fleets/pi/", true},
{"fleets/pi/status/boxes/", "fleets/pi/status/boxes/", true},
{"fleets/other/", "", false},
@@ -419,6 +432,10 @@ func TestStoreDoorRefusalsAreByteIdentical(t *testing.T) {
d, store := newTestDoor(t)
seedObject(t, store, otherAccountKey, "someone else's")
seedObject(t, store, "fleets/other/status/boxes/box-a.json", "another fleet's")
+ // Seeded so the account-manifest row below refuses a key that really
+ // EXISTS: the refusal must not be distinguishable from the missing-key
+ // one, which is only proved against an object that is actually there.
+ seedObject(t, store, "accounts/zz99/account.json", "another tenant's account")
req := func(name, token, method, target string) *http.Request {
return doorReq(method, target, name, token, "")
@@ -437,6 +454,15 @@ func TestStoreDoorRefusalsAreByteIdentical(t *testing.T) {
"list out of scope": req(testChannel, testToken, "GET", "/store/list?prefix=apps/zz99/"),
"unknown path": req(testChannel, testToken, "GET", "/store/nope"),
"wrong method": req(testChannel, testToken, "POST", "/store/o/"+ownKey),
+ // The two classes Task 2 deferred. The account manifest is the key a
+ // tenant-enumeration probe would reach for FIRST (accounts/<sqid>/
+ // account.json is the one object whose presence answers "does this
+ // account exist"), and /store/dirs is the whole second listing route
+ // — its refusals were never pinned to the same shape as /store/list's.
+ "other account's manifest": req(testChannel, testToken, "GET", "/store/o/accounts/zz99/account.json"),
+ "dirs out of scope": req(testChannel, testToken, "GET", "/store/dirs?prefix=accounts/zz99/"),
+ "dirs of another fleet": req(testChannel, testToken, "GET", "/store/dirs?prefix=fleets/other/"),
+ "dirs unauthenticated": req(testChannel, "not-the-token", "GET", "/store/dirs?prefix=fleets/"),
}
type shape struct {
code int