carlos / platform Public

specs: app trust classes + private instances, ahead of their code (#231)

Both designs were approved 2026-08-12 and have lived on the
`trust-placement` branch (PR #183) ever since. That PR is now 72 commits
behind `main` and conflicts in five files across the billing path, so it
is not close to landing — but carlosframework.com wants to publish the
trust-class vocabulary (S / A1 / A2 / A-public / B / C, and the estate
classified in §4), and the site should cite something on `main` rather
than something on a branch.

So the two spec documents land here on their own, with an
implementation-status note on each recording exactly what has NOT
shipped: the compute provisioner, the private-instance walks, box_hours
accrual and the console rent flow all remain on #183, unmerged, with
three deploy preconditions outstanding.

No code changes. PR #183 is untouched and still owns the implementation.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell 59d2d435ab2ed7cf56236cb44ad6156a6fe0b4ca parent 377d533
2 files changed, +455 −0
  • docs/superpowers/specs/2026-08-12-app-trust-classes-design.md +233 −0
  • docs/superpowers/specs/2026-08-12-private-instances-design.md +222 −0
diff --git a/docs/superpowers/specs/2026-08-12-app-trust-classes-design.md b/docs/superpowers/specs/2026-08-12-app-trust-classes-design.md
new file mode 100644
index 000000000..c61b189af
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-12-app-trust-classes-design.md
@@ -0,0 +1,233 @@
+# App Trust Classes: who can see plaintext, and where an app may run
+
+**Date:** 2026-08-12
+**Status:** Approved design (Paul, via brainstorming session)
+**Implementation status (2026-08-19):** these two specs are merged to `main` ahead of their code so the classification vocabulary is citable. The stage-1/2 implementation — compute provisioner, private-instance walks, `box_hours` accrual, console rent flow — lives on PR #183, which is unmerged, conflicting with `main`, and carries three deploy preconditions. Nothing here describes shipped behaviour except the estate classification in §4, which records apps as they are built today.
+**Stage:** 1 of the global-mesh arc (classes → private instances →
+contributing fleets → partner placement). This spec is useful standalone:
+classes gate placement everywhere, mesh or not.
+**Companion:** `2026-08-12-private-instances-design.md` (stage 2 consumes
+the host-tier vocabulary defined here).
+
+## Why
+
+The mesh idea — anyone contributing compute, apps placed wherever capacity
+exists — only works if the platform can answer one question mechanically:
+*what does this box learn by hosting this app?* Today nothing answers it.
+Placement is gated by pools and regions (capacity shape), never by data
+posture (trust shape).
+
+The 11 factors (11factor.org, canonical text — link, don't copy) already
+contain the answer's skeleton. Only **Factor III (Encrypt everything)**
+bears on what a host can observe; Factor VII says explicitly that
+self-hosting "ought not necessarily be required for security"; Factor III's
+closing line — "a weaker version of 'encrypt everything' can also be:
+'separate everything'" — is the textual warrant for a non-E2EE tier. So the
+class keys on **which** factor posture an app holds, never on how many
+factors it employs. The rest of the factors are badges (see §5).
+
+The platform already classifies *services* this way: "blind services — may
+hold data, but only data they cannot read" vs "seeing services — process
+plaintext, therefore forbidden to hold it" (`2026-08-01-carlos-platform-design.md`
+§7, "nothing that sees may store"). This spec is the same idea aimed one
+level down, in the same words.
+
+## 1. The classes
+
+Primary axis: **who can read the plaintext of user data.**
+
+| Class | Name | Definition | Who reads user plaintext |
+|---|---|---|---|
+| **S** | Stateless / static | Holds no user data: a static file tree, or a binary with no persistent user state. | n/a — nothing to see |
+| **A** | Host-blind | Keys are held by the user's devices (or explicitly-enrolled member devices). The instance process never holds a key that opens user content. Total compromise of the running host yields ciphertext. | Nobody but the user's own devices and invited members |
+| **B** | Operator-sighted (*"sealed single-tenant"*) | User data is sealed everywhere it rests outside the live process — parked DB, WAL replica, blobs. The live instance holds a DEK in memory while awake because unattended automation requires plaintext. Cold artifact leak yields nothing; live-host compromise yields data. | The operator of the running host, while the instance is awake |
+| **C** | Host-sighted | The app holds user plaintext at rest as a deliberate, recorded decision — support visibility, SQL over PII, payments. Confidentiality rests on operator policy and tenant isolation, not mathematics. | Anyone with root on the box, or the DB file, at any time |
+
+The B definition is Seapointish's shipped "sealed single-tenant" posture,
+adopted verbatim — it is documented, honest about its residual risk, and
+tested (`TestNoPlaintextAtRest`). Do not coin a new phrase.
+
+**Ruling (Paul, 2026-08-12): B1/C1 collapse.** The estate contains exactly
+two non-E2EE postures — sealed-single-tenant (Seapointish) and host-sighted
+(Titogo). Numbered sub-levels below B and C are dropped until a genuinely
+distinct third posture ships. (Titogo's relay-KEK token sealing is
+credential custody, not user-data sealing; it does not make Titogo B.)
+
+### A-grades: metadata residue
+
+**Ruling (Paul, 2026-08-12): the sub-number inside A grades metadata
+residue**, not cloud-dependence as in the original sketch:
+
+- **A1** — the only residue is transport shape: who connects when, object
+ sizes, timing. No content-derived, application-level plaintext
+ identifiers or relationships at rest. (Slopbox is the estate's only A1
+ today: padded fixed-size blobs, encrypted filenames.)
+- **A2** — content sealed, plus a **declared, enumerated application-level
+ residue** at rest: Keymail's routing headers, Eleven's thread-membership
+ graph, Kass's clear log timestamps.
+
+Every A app publishes a **metadata residue statement** (Eleven's README and
+Keymail's README are the existing templates). An *undeclared* plaintext
+surface is a class violation regardless of intent — the declaration is what
+makes the class auditable. Both grades are mesh-eligible; the number is
+truth-in-labeling shown to users, not a placement gate.
+
+Note recorded for honesty: a relay-based messenger structurally knows who
+shares a thread with whom. A1's "no content-derived metadata" wording is
+deliberate so that Eleven can reach A1 by finishing `docs/no-plaintext.md`
+while still carrying a one-line thread-membership residue note.
+
+### A/public: public-by-design plaintext
+
+**Ruling (Paul, 2026-08-12).** An A-class app whose plaintext surfaces are
+public by design (Woodstar's kind-1 posts — a public microblog) carries the
+**`A/public`** flag with those surfaces enumerated. A mesh box hosting
+public posts learns nothing a browser doesn't; demoting such apps to B
+would misstate the placement consequences. Undeclared plaintext remains a
+violation — the enumeration is the contract.
+
+### Class attaches to a workload, not an app
+
+Sidecars force this. Eleven's E2EE instance is A; its operator-run bot
+sidecar is an ordinary member whose "device" holds its own key — the
+sidecar workload is C. Kass's agent likewise. One app, two placement
+answers. The stamp (§6) therefore attaches per workload kind
+(instance / sidecar / service / static), with the app-level class defined
+as the *most exposed* of its workloads for display purposes.
+
+## 2. Host tiers
+
+Keyed on **who controls the host** — control, not payment (ruling, Paul,
+2026-08-12: a platform-provisioned private instance paid for by a customer
+is still an operator-controlled box; buying isolation does not change who
+holds root).
+
+| Tier | Who controls the box | Examples |
+|---|---|---|
+| **0** | The deployment operator: operator's hardware or cloud account, operator root. | Flagship boxes, dedicated pools, platform-provisioned private instances |
+| **1** | A named legal counterparty under a data-processing agreement; box identity is a minted, revocable fleet token. | Partner fleets (Eleven's boxes, Tito's spare capacity). A customer's own fleet running the customer's own apps is the degenerate case: no third party, no DPA needed. |
+| **2** | A stranger. No legal relationship, no recourse, no audit. | Open contribution mesh |
+
+## 3. The placement rule
+
+Classes order `S ≤ A < B < C`; tiers order `2 < 1 < 0`. Placement is legal
+iff the box is at least as trusted as the class requires:
+
+| | Tier 0 | Tier 1 (DPA) | Tier 2 (mesh) |
+|---|---|---|---|
+| **S** | ✓ | ✓ | ✓ |
+| **A** (A1, A2, A/public) | ✓ | ✓ | ✓ |
+| **B** | ✓ | ✓ | ✗ |
+| **C** | ✓ | only where the DPA names the data categories | ✗ |
+
+In the platform's idiom: *nothing that sees may be placed where nobody is
+accountable for seeing.*
+
+## 4. The estate, classified
+
+| Workload | Class | Evidence anchor |
+|---|---|---|
+| Eleven (instance) | **A2** today; A1 when `docs/no-plaintext.md` completes (README still declares metadata in clear as of 2026-08-12) | `messenger/README.md`, `docs/no-plaintext.md` |
+| Eleven (bot sidecar) | C | member-keyed sidecar |
+| Keymail | A2 (residue: from/to/date routing headers) | `keymail/README.md` |
+| Kass (instance) | A2 (residue: clear log timestamps; open item below) | `kass/internal/store/instance.go` |
+| Kass (agent sidecar) | C | `kass/CLAUDE.md` |
+| Slopbox | A1 | `slopbox/DESIGN.md` (padded, content-blind) |
+| Woodstar (relay) | A2 + A/public (public: kind-1 posts; sealed: blobs, DMs) | `woodstar/DESIGN.md` |
+| Amadan | A2 pending contradiction (repo one-rule says never-plaintext; story deck says "E2EE optional" — Paul to reconcile) | amadan worktree CLAUDE.md |
+| Seapointish | B — the defining example | `seapointish/README.md` |
+| Titogo | C — the defining example | `titogo/docs/sign-in.md` |
+| carloku.com, platform site, carlosframework.com, story deck, hello | S | static / stateless |
+
+**Open item (Kass):** `sign_in_links.email` is a plaintext address at rest
+until expiry, in a registry DB that litestream ships to a bucket. Proposed
+rule for every A app's auth surface: a **bounded transient-auth allowance**
+— plaintext auth material permitted only with a declared TTL and eviction;
+kass's row qualifies once eviction is verified, and the allowance goes in
+its residue statement. Needs Paul's confirmation on the kass side.
+
+## 5. Badges, not levels
+
+`self_hostable`, `runs_locally`, `requires_service: [svc/mail, …]`,
+`open_source`, `agent_optional` are recorded and displayed but are **not**
+inputs to placement. A required cloud dependency changes what a *third
+party* learns — a sub-processor question about that service, already
+covered by the blind/seeing service classes — not what the hosting box
+learns. Factor VII forbids treating self-hostability as a trust level.
+
+## 6. Declaration, stamp, and enforcement
+
+- **Declared** in `apps/<sqid>/<app>/instances.json` (`provision.Manifest`)
+ — the object that already exists to be "the policy every record is
+ checked against." Additive field `trust_class`; **absence means C, never
+ A** — an old manifest can never accidentally become mesh-eligible.
+ Workload granularity: `trust_class` for the instance workload,
+ `sidecar_trust_class` when a sidecar exists.
+- **Mirrored** by the console into a root-key-signed per-app object in the
+ `production.json` mould (`{"schema":1, "account", "app", "trust_class",
+ "actor", "set_at"}`) — a scoped signing key must not be able to mint a
+ class change.
+- **S is derived, never declared**: `Kind == "static"` in the release
+ manifest, checked by `internal/vet` at promotion. The only class the
+ platform can assert without trusting anybody.
+- **Enforced** at the two existing placement chokepoints:
+ `provision.Reconcile`'s jurisdiction check gains a
+ `classAdmitted(cfg.TrustTier, rec.Class)` conjunct, and
+ `placeAppTarget` refuses a placement target whose fleet's tier the class
+ forbids. Boxes declare `CARLOS_TRUST_TIER=0|1|2` in `host.env` exactly as
+ they declare `CARLOS_POOLS`; **an unconfigured box defaults to tier 2**
+ (least trusted). Operator-side, a fleet's tier (and DPA reference) is a
+ fleet attribute; `fleet_boxes.tags` is the interim display surface.
+- **Ratchet (ruling, Paul, 2026-08-12): loosening evicts.** Tightening
+ (C→A) applies immediately. Loosening (A→C) triggers re-placement of
+ instances off now-illegal hosts before the new class takes effect —
+ never grandfathered, mirroring the bake-flag ratchet's stolen-key
+ reasoning in the opposite direction.
+- **Advisory-first rollout**: divergence between declared class and
+ observed reality surfaces as an advisory (the
+ `warnExternalSockets` / `adviseBackingMismatches` precedent), hardening
+ to refusal once the verification pass (§7) has run clean across the
+ estate.
+
+## 7. Verification: promise only what is falsifiable
+
+Verifiable now or nearly:
+- **S**: derived from the vetted manifest. Done at promotion.
+- **No plaintext at rest, in objects the platform holds**: an A-class app
+ registers a canary token; a class-check pass samples the app's parked
+ DBs/blobs/WAL in the bucket for it — the family's own test shape
+ (Keymail's grep, Amadan's `AMADAN_CANARY_7f3a9`, Seapointish's
+ `TestNoPlaintextAtRest`) run platform-side. "Publish their state and see
+ what leaks."
+- **A-class log obligation**: no user content in logs (logs ship plaintext
+ to the bucket via the fleet ACL); same canary sampling applies.
+
+Not verifiable today — the spec must not promise it:
+- **No-egress.** There is no network capability and no egress rail
+ (`internal/caps` has no such vocabulary; no `IPAddressDeny` anywhere).
+ "The app can't phone home" is unfounded until a rail exists.
+- **Key custody.** Client-side custody cannot be proven from outside;
+ it stays a declaration backed by open source and the app's own published
+ canary test.
+
+## 8. Mesh prerequisites this work surfaced (recorded, not solved here)
+
+1. **`CARLOS_SECRETS_KEY` is deployment-wide, present in every host's
+ `host.env`, and the adopt pass writes decrypted app env files to disk.**
+ A tier-2 box would hold the key to every app's secrets in the
+ deployment, class notwithstanding. **The open mesh is not viable at any
+ class until secrets are re-keyed per box or per app.** File as its own
+ issue; it shapes intervening work.
+2. **IP-blindness is currently accidental** — stdlib Rewrite-mode proxying
+ drops `X-Forwarded-For` on the tunnel hop, failing safe. The runbook's
+ "future fix" to forward it must become a per-tier prohibition: tier 1/2
+ boxes never receive client identity.
+3. Tier-2 placement additionally awaits the contribution/settlement design
+ (stage 3) — this spec only defines the vocabulary it will gate on.
+
+## Out of scope
+
+The mesh itself (registration, settlement, eviction mechanics), DPA
+tooling, the secrets re-key design, the egress rail, and any change to how
+apps are built. Stage 2 (private instances) is specced separately and
+depends only on §2's tier vocabulary.
diff --git a/docs/superpowers/specs/2026-08-12-private-instances-design.md b/docs/superpowers/specs/2026-08-12-private-instances-design.md
new file mode 100644
index 000000000..e3e93ffdb
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-12-private-instances-design.md
@@ -0,0 +1,222 @@
+# Private Instances: platform-provisioned dedicated boxes
+
+**Date:** 2026-08-12
+**Status:** Approved design (Paul, via brainstorming session)
+**Implementation status (2026-08-19):** these two specs are merged to `main` ahead of their code so the classification vocabulary is citable. The stage-1/2 implementation — compute provisioner, private-instance walks, `box_hours` accrual, console rent flow — lives on PR #183, which is unmerged, conflicting with `main`, and carries three deploy preconditions. Nothing here describes shipped behaviour except the estate classification in §4, which records apps as they are built today.
+**Stage:** 2 of the global-mesh arc. Depends on
+`2026-08-12-app-trust-classes-design.md` for host-tier vocabulary only.
+**Builds on:** customer fleets v1 (`2026-08-05-customer-fleets-design.md`),
+fleets console (`2026-08-10-fleets-console-design.md`), fleet API surface
+(`2026-08-11-fleet-api-surface-design.md`, merged), unit economics
+(`2026-08-11-unit-economics-design.md`, merged), object storage primitive
+(`2026-08-10-object-storage-primitive-design.md` — the provisioning
+pattern), platform spec §6 pools/dedicated-tier vocabulary.
+
+## The product
+
+A member picks a number of boxes, regions, and instance types on the
+console; the platform provisions dedicated EC2 boxes in the **operator's
+AWS account** and attaches them to the member's account as a managed
+fleet. The member's apps are placed on them exactly as on a BYO fleet.
+"Customer fleet, except the platform runs the metal."
+
+**Rulings (Paul, 2026-08-12):**
+- **Operator's cloud account.** Private instances are tier-0 hosts (the
+ operator controls root), so they may carry **B and C apps** under the
+ customer's existing DPA with the deployment. Buying isolation does not
+ change who holds root. The variant that provisions into the *customer's*
+ cloud account is a different product (customer-controlled host, their
+ bill, no automatic B/C carriage) and is **explicitly out of scope**.
+- **Billing = `box_hours` meter.** Rent is a new additive meter —
+ `box_hours` × per-(provider, region, type) rate — flowing through the
+ existing `Statement`/`Scenario` machinery. EC2-like: hourly while
+ provisioned. Reservations/commitments arrive later as discounts on the
+ same meter, not as a subscription object.
+- **No customer shell access.** Matches the no-SSH-ingress box design; the
+ console and CLI are the entire interface. Said out loud in the product
+ copy.
+- **Deletion terminates the box, parks the bucket.** Returning a rented
+ box destroys the EC2 instance immediately; the fleet data bucket
+ survives behind a retention window (default 30 days) before reap, so
+ "returned the box" is never "lost the data" on the same day.
+- **arm64-only Graviton catalog at v1** (t4g/m7g families). The build
+ pipeline ships linux-arm64; a wrong-arch box resolves no artifact at
+ all. x86 providers (DigitalOcean has no arm64) are gated on multi-arch
+ build work, not on provisioner code — recorded so provider expansion is
+ understood to be builds-first.
+- **Multi-provider designed-for, AWS-only built.** Provisioning goes
+ behind a `ComputeProvisioner` seam (§3); DO/Hetzner/GCP are future
+ implementations of the same interface. Tofu stays operator-side for the
+ durable estate; the runtime never embeds tofu (no state files, no
+ runtime provider downloads — the objstore precedent, extended).
+
+## 1. Shape: a managed fleet
+
+A private-instance pool is a **fleet** owned by the member's account, with
+one new bit: `managed` — the platform provisioned it and owns its cloud
+resources. Everything merged for fleets applies unchanged: hashed box
+tokens, dynamic edge channel auth, 5s pings, store-door access (no AWS
+credentials against the platform bucket, self-update via
+`bootstrap/carlos`), placement via `place=fleet/<name>`, dark-fleet 503,
+advisory bindings with the S3 lease as authority. A managed fleet's boxes
+need **no certs and no inbound ports** — traffic arrives through the edge
+tunnel like any fleet box.
+
+New registry/record state (additive):
+- `fleets.managed` (bool) — platform-provisioned; managed fleets refuse
+ `add-box` of BYO boxes (one fleet is one trust story).
+- A **box-as-product record** per box: `{account, fleet, label, provider,
+ region, instance_type, cloud_instance_id, provisioned_at,
+ terminated_at}` — the record `box_hours` accrues against and the
+ deletion walk reads. Lives console-side (the console spends the money
+ and owns the ledger), mirrored to the bucket for the box's own
+ visibility.
+- `fleet_boxes` rows for managed boxes carry the instance type in tags
+ until `status.BoxMetrics` gains a class/type field (additive), which
+ also lets `BoxSpec` in economics stop being hand-entered.
+
+## 2. Bootstrap: cloud-init only, no SSM
+
+The console mints the fleet token (`mintFleetToken`) at the same moment it
+calls the provisioner, so **everything a box needs at first boot bakes
+into user-data**: create user/dirs (the host-init shape), fetch the binary
+via the store door (`bootstrap/carlos` — the sanctioned no-credentials
+path), write `host.env` (fleet contract: `CARLOS_FLEET`, `CARLOS_FLEET_BOX`,
+`CARLOS_REGION=fleet/<name>`, `CARLOS_RELEASE_PUBKEY`, `CARLOS_TUNNEL_URL/
+_NAME/_TOKEN`, `CARLOS_TRUST_TIER=0`, `CARLOS_STATUS_PREFIX=fleets/<name>/`),
+run `carlos bootstrap` (plain: the box authenticates via its instance
+profile; `-offcloud` exists for boxes without one), start `carlos-edge`.
+The operator-run SSM bootstrap step disappears for managed boxes — which
+is what the no-box-commands rule requires anyway. AMI selection is pinned
+per region in the catalog (§4), never `most_recent` at provision time (the 2026-08-03
+"destroy the production box" lesson).
+
+No SSM agent ships on a managed box at all: the fleet instance profile
+grants no `ssmmessages` permissions, so the agent could never connect even
+if installed. The substitute for a box with no SSM and no SSH is a
+console breadcrumb — every failure path (plus an ERR trap) and success
+write one line to `/etc/motd` and `/dev/console`, readable with zero box
+access via `aws ec2 get-console-output`.
+
+Managed boxes in the operator's account still use an **instance profile**,
+not a minted IAM user — scoped to the fleet's data bucket only, same
+boundary as `region-remote-host` but without long-lived keys.
+
+## 3. ComputeProvisioner
+
+Mirrors `objstore.Provisioner` exactly: an interface with an AWS
+implementation and a fake; **idempotent by contract** ("already exists"
+and "already gone" are successes; deterministic naming/tagging by
+`(fleet, label)` replaces state files); sync walk in the handler, no
+reconciler. Verbs: `EnsureBox` (RunInstances with user-data, SG,
+instance profile), `DescribeBox`, `TerminateBox`, plus reuse of
+`objstore.EnsureBucket` (+ the missing hardening: versioning, SSE,
+public-access block) for the fleet data bucket.
+
+Authority: a new scoped pinfra grant to the console box role —
+`ec2:RunInstances/TerminateInstances/Describe*` restricted by resource
+tags (`carlos:managed-fleet=*`) and by instance profile passrole, in the
+`tito/object-store.tf` mould. Fatal-at-boot coordinates
+(`CARLOS_COMPUTE_BACKEND=aws`, region allowlist), fail-closed like
+`CARLOS_STORE_*`.
+
+**Spend authority and admission.** Store provisioning is operator-gated;
+box rental is *members spending the operator's money continuously*, so it
+gets its own gate, not a widened operator list: per-account box quota
+(default small, operator-raised), a priced catalog the member must select
+from (no free-form instance types), and the write limiter + audit line on
+every provision/terminate. A failed provision walk is repaired by re-run
+(idempotence), never by a background reconciler.
+
+## 4. Catalog and pricing
+
+A bucket object `economics/catalog.json` (root-signed, versioned):
+rows of `{provider, region, instance_type, arch, hourly_usd, ami}`. The
+console renders it; `carlos boxes create` validates against it; the
+`box_hours` meter prices from the catalog row captured **at provision
+time** onto the box-as-product record (price changes never silently
+reprice running boxes). This is the first published price surface — it
+consciously retires carloku's "no pricing language until billing exists"
+rule for exactly this product, nothing else.
+
+Economics integration (small, necessary): the hourly sweep learns
+fleet-prefixed telemetry (`fleets/<f>/metrics/boxes/…`,
+`fleets/<f>/usage/edge/…`) so managed boxes appear on the operator
+dashboard; a managed box reports as **both** a cost line (AWS bill) and a
+revenue line (`box_hours` × rate) — margin per box becomes visible for
+free. Idle on a dedicated box is the customer's to spend, never smeared.
+
+**`box_hours` vs compute telemetry — never bill both (billing-engine
+ruling, 2026-08-15).** A managed box's compute telemetry
+(`instance_hours` / `cpu_core_hours` / `mem_gb_hours`) still accrues onto
+the renter's statement alongside `box_hours`, because the sweep attributes
+a fleet box's units to the fleet's owning account. That compute telemetry
+is retained for the operator's **utilization** view only and MUST NOT be
+billed to the customer alongside `box_hours`: `box_hours` is the
+customer-facing rental meter, and the future billing engine bills
+`box_hours` **or** compute usage for a given box, **never both** — pricing
+a flat rental rate and the same box's metered compute would double-bill the
+customer. There is no double-charge today (nothing prices `box_hours` yet);
+this records the rule so the biller is built to honour it.
+
+A rented-box tenant's **account** revenue column on the dashboard reflects
+cost-plus on modelled compute usage, **not** the flat rental price the
+customer actually pays for the box — that flat price is the box row's
+revenue (`box_hours` × captured rate). The two views answer different
+questions and are not meant to sum.
+
+## 5. CLI and console surface
+
+- `carlos boxes create -fleet <name> -region <r> -type <t> [-count n]` —
+ creates the managed fleet on first use; prints per-box progress and the
+ final placement hint. Completes the job end-to-end (no follow-up verb
+ needed to verify: the command waits for the box's first heartbeat).
+- `carlos boxes ls` / `carlos boxes rm -fleet <name> -label <l>` —
+ rm = terminate + park bucket behind retention; prints both facts.
+- Console: the account Fleet tab gains a "Rent boxes" flow next to BYO;
+ per-box page shows type/region/hourly rate/accrued `box_hours`.
+- Placement is unchanged: `carlos apps place -app X -target fleet/<name>`.
+
+## 6. Honesty items (product copy and docs)
+
+- **Single-box apps 503 when the box is down; nothing migrates.** ≤10s
+ detection, `Retry-After: 2`, no automatic replacement in v1. Multi-box
+ fleets get the existing deterministic fallback. HA is the HA-posture
+ work, not this spec.
+- The flagship edge remains a shared SPOF (#153) — a private instance
+ buys isolation and reserved capacity, not availability.
+- Apps behind the tunnel see the edge's IP, not the client's; WebSockets
+ fail closed. Both pre-existing fleet properties, stated in the rental
+ copy.
+
+## 7. Dependencies and stacking
+
+1. **PR #137 (route records / fleet-route materialization)** — hard
+ dependency. Until placement mints the platform-side route without an
+ operator running `carlos add` on the edge box, nothing here is
+ self-serve. Stage 2 stacks on #137, not around it.
+2. Economics sweep fleet-prefix change (§4) — small, ships with this.
+3. Instance-type in box telemetry (additive `status.BoxMetrics` field) —
+ ships with this; retires hand-entered `BoxSpec` for managed boxes.
+4. Trust classes (stage 1) — only the tier stamp (`CARLOS_TRUST_TIER=0`)
+ is consumed; private instances ship even if class enforcement is still
+ advisory.
+
+## Genuinely new build (nothing else is)
+
+`ComputeProvisioner` + AWS impl + fake; box-as-product record + quota
+gate; catalog object + pricing capture; `box_hours` meter; `carlos boxes`
+verb family + console rent flow; cloud-init template; pinfra EC2 grant;
+sweep prefix + BoxMetrics additions; deletion walk (terminate → park →
+retention reap, reusing the releases-retention reap shape).
+
+## Out of scope
+
+Provisioning into customer cloud accounts; non-AWS providers (seam only);
+x86/multi-arch builds; reservations/commitment pricing; automatic
+replacement or migration of dark boxes; payments/invoices (the meter
+records truth; collecting money stays manual until a payments design);
+mesh contribution of private instances (stage 3 — but the tier-0
+consequence is recorded there: contributed private instances remain
+operator-controlled and could take B/C overflow under the existing DPA
+umbrella).