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>
2 files changed,
+455
−0
docs/superpowers/specs/2026-08-12-app-trust-classes-design.md+233 −0docs/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). |