eleven / messenger Public
π€ Managed AI on provisioned spaces β design
Status: design agreed 2026-07-14 (decisions below are Paul's). Built so far: phase 0 (#806 fix + Go invite mint, PRs #864/#865), phase 1 (the usage pipe, PR #867), phase 2 (attach-bot + the onboarding opt-in, #858). Phases 3β5 are unbuilt. 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.envfile, 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)
- 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).
- 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.
- Small included allowance funded from a capped pool β free AI is a bounded trial, never an unbounded subsidy.
- On-box first, then iterate β sidecars stay co-located with instances (today's trust posture) until the bot-host tier exists.
- 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=falseunless 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 runmust 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-botshells out toagent/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
usageblock ininternal/bot/anthropic.go(todayanthResponsedoesn'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/usageauthed 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)besidestorage_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'ssetFeaturesbridge 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 theadd-botlogic: mint the Eleven invite, stage/data/<host>-bot.envwith the fleetANTHROPIC_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.envwhile 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 indocs/tier-economics.mdlike 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_logcreation-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
AIEnabledgate 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 (
minEveryfloors 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-spaceDynamicUser). - 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; theX-Lchat-Sidecarmarker 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; thelchatbinary (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):
- Anthropic direct β the existing hand-rolled Messages client; stays the default for BYO key and self-hosters (
ANTHROPIC_BASE_URLseam exists). - Bedrock β for the managed VPC tier. The draw is not model choice: it's IAM-role auth (no
ANTHROPIC_API_KEYexists 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.goproves the no-dependency signer pattern; model ids gain theanthropic.prefix. Check eu-west-1 / EU cross-region profile availability β EU-resident inference is a real selling point given bots see plaintext. - 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_enabledstill 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.