eleven / ocho Public

docs: managed-AI provisioning design β€” opt-in, metering, billing, VPC bot tier, BYO key (#855)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell dda6365bb42884b4aab178f91895d0acf8425ad7 parent 95ae9ff
1 file changed, +253 −0
  • docs/ai-provisioning.md +253 −0
diff --git a/docs/ai-provisioning.md b/docs/ai-provisioning.md
new file mode 100644
index 0000000..287a23e
--- /dev/null
+++ b/docs/ai-provisioning.md
@@ -0,0 +1,253 @@
+# πŸ€– Managed AI on provisioned spaces β€” design
+
+Status: **design agreed 2026-07-14** (decisions below are Paul's); nothing in
+this document is built unless a phase says so. This is the plan for making the
+onboarding "enable AI" answer *do* something on self-serve spaces: provision a
+sidecar, meter what it spends, bill it honestly, and eventually move the whole
+bot trust plane off the serving box.
+
+Related: `docs/bots.md` (trust model), `docs/member-bots.md`,
+`docs/cloud-provisioner.md` (the attach-bot gap this closes),
+`docs/planet-scale.md` (co-spawn + the sidecar invariant),
+`docs/tier-economics.md` ("AI is never bundled"), issues #562 (pricing),
+#668 (usage metering pipe), #685 (evict the trust plane), #806 (sidecar
+self-provisioning bug).
+
+## The gap today
+
+The onboarding toggles are real gates β€” `bots_enabled` 403s new-agent creation
+(`main.go`) and member-bot charters (`memberbots.go`); `ai_enabled` is a
+genuine kill-switch the sidecar honors on every reconnect
+(`internal/bot/brain.go`, via `GET /api/bot-config`). But a freshly
+provisioned `<sqid>.11ch.at` space has **no sidecar and no model key**, so
+answering "yes, enable AI" produces nothing: Eleven never appears. The
+provisioner chain (`cloud.go` β†’ `controld.go` β†’ `addInstance`) contains zero
+bot hooks.
+
+Meanwhile most of the machinery already exists:
+
+- `lchat add-bot` (`bot.go`) automates invite minting, the `-bot.env` file,
+ ownership, and the unit β€” or defers to co-spawn on hibernating hosts.
+- The activator **co-spawns** sidecars with their instance and makes sidecar
+ traffic activity-invisible (`internal/bot/sidecarmark.go`), so a managed
+ sidecar can't hold a space awake or wake it.
+- The storage meter (`meter.go`) is a proven per-space measure β†’ record β†’
+ enforce loop keyed on the cloud ledger.
+- Per-bot model choice already ships (`bot_configs.model`, admin UI menu,
+ re-read on reconnect); charters carry a per-task override.
+
+## Decisions (Paul, 2026-07-14)
+
+1. **Eleven only** is provisioned for cloud spaces. Admins can add further
+ agents through the existing admin "Add an agent" path; on managed spaces
+ that path must feed the managed sidecar automatically (see
+ *Roster follows the admin* below).
+2. **Bedrock is phased**, not first: phases 0–3 ship on the existing direct
+ Anthropic key; Bedrock lands with the off-box VPC tier where IAM roles and
+ VPC endpoints actually pay off.
+3. **Small included allowance funded from a capped pool** β€” free AI is a
+ bounded trial, never an unbounded subsidy.
+4. **On-box first, then iterate** β€” sidecars stay co-located with instances
+ (today's trust posture) until the bot-host tier exists.
+5. **Bring-your-own-key is an advanced admin option** β€” paste-a-key first,
+ "click to connect" via an OAuth-style flow when a provider supports one.
+
+## Trust model and disclosure
+
+Managed AI means *we* run a machine that holds the bot's private key and can
+decrypt exactly the threads the bot is in. That is already the disclosed
+exception-by-membership in `docs/bots.md` and already true for the
+hand-provisioned fleet β€” but self-serve opt-in turns it into a consumer-facing
+promise, so:
+
+- **AI is opt-out by default.** The setup flow writes `ai_enabled=false`
+ unless the admin takes a specific opt-in step. (Deterministic bots β€” the
+ command floor, no model, nothing sent anywhere β€” can stay default-on.)
+- **The opt-in copy discloses plainly**, in the lock-and-key voice: the
+ helper's key lives on Eleven's machines, and it can read the chats it's
+ invited to β€” that's what makes it able to help. Membership stays visible in
+ the roster.
+- The invariants hold throughout: bot code never runs in the server process,
+ the sidecar stays a pure outbound client, and **the instance server never
+ stores a usable model key** (load-bearing for BYO key, below).
+
+## Phase 0 β€” zero-touch provisioning that works
+
+Prerequisites for everything else; both PR-sized.
+
+- **Fix #806**: `bot run` must redeem the invite when a session path is set,
+ the file is absent, and the matching invite env var is present (option 1 in
+ the issue β€” it also covers the co-spawn path, and it is what makes keys
+ *born on the host that runs the bot*, which phase 4 relies on).
+- **Mint agent invites in Go.** `add-bot` shells out to
+ `agent/mint-invite.mjs`, dragging Node onto any box that provisions. Port
+ the mint to the control binary.
+
+## Phase 1 β€” meter before money (#668)
+
+Measure first, price from data. No billing in this phase.
+
+- Parse the `usage` block in `internal/bot/anthropic.go` (today
+ `anthResponse` doesn't even deserialize it). Record all four counters β€”
+ input, output, cache-write, cache-read β€” they're priced differently.
+- Aggregate in the sidecar per (day, model, dimension); flush to a new cloud
+ `POST /api/usage` authed by a **per-instance meter token** minted at create
+ time (mint/list/revoke pattern copied from the push relay). Ledger:
+ `usage(space_id, day, dimension, quantity)` beside `storage_bytes`.
+- Add bot-activity dimensions (replies, charter runs) on the same pipe.
+- Run silently on the existing sidecar fleet for a couple of weeks; the
+ allowance and add-on price in phase 3 are set from this data.
+
+The meter flush doubles as the **budget heartbeat**: the response to
+`POST /api/usage` carries the space's current AI budget state, so no new
+cloud→instance channel is needed for enforcement (phase 3).
+
+## Phase 2 β€” the opt-in that provisions (on-box)
+
+- **Onboarding**: the extras step's AI question becomes an explicit opt-in
+ chip with the disclosure copy; declining (or skipping) writes
+ `ai_enabled=false`. Opting in triggers provisioning *during* setup β€”
+ "Eleven is joining your space…" β€” and Eleven's welcome DM lands seconds
+ later. The same opt-in surfaces in the Admin app for existing spaces (note:
+ the Admin mini-app's `setFeatures` bridge is currently dead code β€” wire it
+ up here).
+- **Signal path**: instance β†’ cloud over the meter-token channel ("this space
+ wants AI") β†’ cloud calls a new controld `POST /instances/{host}/bot`, which
+ runs the `add-bot` logic: mint the Eleven invite, stage
+ `/data/<host>-bot.env` with the fleet `ANTHROPIC_API_KEY`, chown. On a
+ hibernating host that's all it does β€” co-spawn picks the sidecar up (the
+ activator needs one small hook: notice a newly staged `-bot.env` while the
+ child is already awake, so opt-in doesn't wait for the next sleep/wake
+ cycle).
+- **Roster follows the admin**: when an admin adds another agent on a managed
+ space, the same signal path appends that identity's invite to the bot env
+ and restarts the sidecar; post-#806 it self-provisions. The sidecar's fixed
+ two-identity env shape (`LCHAT_INVITE` / `LCHAT_ELEVEN_INVITE`) generalizes
+ to a numbered or manifest form as part of this.
+- Latency budget: chip-tap to welcome DM in seconds, not sweeps β€” this is why
+ the signal is a push over the meter channel, not a stats-poll.
+
+## Phase 3 β€” usage accrues to the subscription
+
+- **Stripe Billing Meters** on the existing per-space subscription
+ (`billing.go` β€” the provisioner's Stripe integration, not the in-space
+ Connect one). Metered price = provider cost Γ— a published markup, recorded
+ in `docs/tier-economics.md` like every other rate. AI becomes the first
+ real marginal-cost line in the public books.
+- **Included allowance from a capped pool**: quantities under the per-space
+ monthly allowance never forward to Stripe; a **fleet-wide monthly AI
+ budget** (mirror of the `creates_log` creation-budget circuit breaker)
+ bounds aggregate subsidized spend no matter how many free spaces opt in.
+ Allowance and pool sizes come from phase 1 data. Cost reality check at
+ today's list prices: a typical cached Sonnet reply runs ~1–1.5Β’, so ~100
+ replies/month β‰ˆ €1.2/month against Family's €4.68/*year* net β€” the
+ allowance is trial-sized by necessity, and "AI is never bundled" stands.
+- **Exhaustion behavior**: the meter-flush response flips the space to
+ budget-exhausted; the sidecar's existing `AIEnabled` gate grows that state β€”
+ the bot drops to the deterministic command floor and says so once, honestly.
+ Per the conduct rule (knowledge.md Β§20), Eleven describes the upgrade path
+ and never posts a payment link.
+- **Abuse guards land with billing, not after**: a per-space daily reply
+ ceiling independent of budget (`minEvery` floors charter schedules but
+ nothing bounds conversational replies), and loop protection stays audited.
+
+## Phase 4 β€” the bot-host tier: egress-only VPC (#685)
+
+The "hardened off-box sidecar tier" from #550 Part 4 v2; sidecars follow the
+commons, not the provider.
+
+- **Sandbox: systemd, not Docker.** The bot unit already carries near-maximal
+ hardening (empty capability set, `ProtectSystem=strict`,
+ `MemoryDenyWriteExecute` β€” possible because the sidecar is pure Go β€” syscall
+ filter, `MemoryMax`), and the sidecar runs no untrusted code: member-app
+ code executes in the *client's* sandboxed iframe, charters are bounded tool
+ loops. Docker would add a daemon and an image pipeline β€” against the
+ one-boring-binary rule β€” for less isolation than the unit provides. The
+ real gap is that **co-spawned** sidecars inherit the router's sandbox;
+ moving them to a dedicated host puts each space back under its own hardened
+ unit (optionally per-space `DynamicUser`).
+- **Host shape**: EC2 (Graviton, instance-role pattern from
+ `infra/dev/compute.tf`) in a **private subnet β€” no public IP, no inbound
+ anything**; egress via NAT restricted to 443. The sidecar is already a pure
+ outbound client; the `X-Lchat-Sidecar` marker means off-box sidecars can't
+ wake or hold instances awake, and charter due-time wakes already work from
+ off-box (`POST /api/charter-next-due`). The IaC boundary test holds: tofu
+ builds the box and role; the `lchat` binary (a bot-host mode) learns which
+ spaces it hosts by outbound-polling the control plane.
+- **Keys are born on the bot host.** Provisioning hands the host one-time
+ invites; the sidecar self-provisions (#806 fix), so bot private keys never
+ transit anywhere. Combined with #685 this evicts bot keys *and* the model
+ key from the serving box.
+- Lifecycle v1: off-box sidecars run always-on (tens of MB each; one small
+ host carries dozens; scale horizontally before getting clever).
+- Ops debt that must close before this scales: **#811** β€” awake bot-sidecar
+ instances never pick up auto-updates.
+
+## Phase 5 β€” the provider seam: Bedrock, local models, catalog
+
+One small interface in `internal/bot` (today's client becomes implementation
+#1):
+
+1. **Anthropic direct** β€” the existing hand-rolled Messages client; stays the
+ default for BYO key and self-hosters (`ANTHROPIC_BASE_URL` seam exists).
+2. **Bedrock** β€” for the managed VPC tier. The draw is not model choice:
+ it's **IAM-role auth** (no `ANTHROPIC_API_KEY` exists anywhere for managed
+ spaces β€” the shared-key blast radius disappears) and **VPC interface
+ endpoints** (model traffic never touches the public internet). Same
+ Messages body under SigV4 β€” `home/sigv4.go` proves the no-dependency
+ signer pattern; model ids gain the `anthropic.` prefix. Check eu-west-1 /
+ EU cross-region profile availability β€” EU-resident inference is a real
+ selling point given bots see plaintext.
+3. **OpenAI-compatible local** β€” the SLM companion direction: a small model
+ in the same private network, called by the sidecar. First workload to
+ move: the addressing classifier (hardcoded Haiku today, `brain.go`) β€”
+ highest volume, lowest difficulty; moving it in-network cuts both spend
+ and plaintext egress.
+
+**Model management**: per-bot choice already exists; the improvement is a
+fleet-served **model catalog** (menu entries + ids) so `admin.js` stops
+hardcoding model ids and fleet-wide swaps/deprecations need no client
+redeploy. Keep the menu small: default, better, cheaper.
+
+## Bring your own key (advanced admin option)
+
+- **The invariant shapes the design**: compromising the server must not yield
+ the model key (CLAUDE.md), so a BYO key is **never persisted in the
+ instance DB**. Set flow: admin UI β†’ instance (held in memory only, never
+ stored, never logged) β†’ cloud over the meter channel β†’ controld writes it
+ into `/data/<host>-bot.env` β†’ sidecar restart. Same at-rest posture as the
+ fleet key today; treated like the admin token in transit.
+- **BYO spaces don't meter and aren't billed** for AI (#668's rule: their
+ key, their cost). The allowance/budget machinery disarms; `ai_enabled`
+ still works as the kill switch.
+- **Click to connect**: Anthropic offers no third-party OAuth key
+ provisioning today, so v1 is paste-a-key (Anthropic key, Anthropic wire
+ format). The **connect broker** (`lchat-connect`, which already holds OAuth
+ client pairs for GitHub/DNSimple) is the natural seam when a provider
+ supports key issuance by consent β€” OpenRouter's PKCE flow exists today if
+ we ever want it, with its own disclosure (a third party would see the
+ bot's plaintext); an Anthropic OAuth flow is a watch item.
+- Advanced means advanced: buried in the admin console, not in onboarding.
+- v2 candidates: per-space Bedrock via cross-account role (Business tier),
+ arbitrary OpenAI-compatible base URL + key for self-managed local models.
+
+## Kill switches and guards (summary)
+
+- Per-space: `ai_enabled` (exists, enforced in the sidecar), budget
+ exhaustion (phase 3), daily reply ceiling (phase 3).
+- Fleet: the subsidy-pool circuit breaker (phase 3); env kill on bot hosts.
+- Egress: charter/research URL fetches from our infrastructure need an
+ egress policy on the bot host (a prompt-injected charter fetching internal
+ endpoints is SSRF from our VPC) β€” the serving box's unfurl SSRF guard has
+ no sidecar equivalent today.
+- Terms: one pass over Anthropic/Bedrock commercial terms (resale with
+ markup, data-retention posture we advertise) before the price goes public.
+
+## Sequencing
+
+Phase 0 β†’ 1 β†’ 2 ship independently and in order; 3 needs 1's data and 2's
+opt-in; 4 and 5 ride the #685 arc and don't block 2–3. Native parity is
+mostly free (onboarding/admin render through the webview) but the opt-in flow
+gets an explicit iOS verify. `docs/tier-economics.md` gains the AI rate line
+when phase 3 prices are set, and its stale "a bot-sidecar space never sleeps"
+note gets reconciled with co-spawn (#670) and the observed #811 behavior.