eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

πŸ€– 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.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.