| 1 | # π€ lchat agent sidecar |
| 2 | |
| 3 | One headless process that hosts an instance's **default agents**, each a normal |
| 4 | E2EE member with its own keypair and identity file. **The lchat server only |
| 5 | ever sees ciphertext** β private keys and your `ANTHROPIC_API_KEY` live only on |
| 6 | this host. It shares `web/static/crypto.js` verbatim (imported directly), so |
| 7 | its wire format is guaranteed to match the app. |
| 8 | |
| 9 | > **The default sidecar is now Go** β `lchat bot run`, a subcommand of the same |
| 10 | > single static `lchat` binary, no Node runtime (`internal/bot/`, see |
| 11 | > `docs/bots.md`). It's a full drop-in: same wire format, and session/state/ |
| 12 | > recipe files are byte-compatible, so it can take over a Node bot's identity in |
| 13 | > place. |
| 14 | > |
| 15 | > **This Node sidecar is kept on purpose** as a runnable **reference |
| 16 | > implementation** β a worked example of how to run a bot in Node if you'd |
| 17 | > rather (it imports `web/static/crypto.js` verbatim, so it's also the clearest |
| 18 | > illustration of the exact E2EE wire format a client speaks). It is not |
| 19 | > deprecated and not scheduled for removal; keep it working alongside the Go |
| 20 | > port. |
| 21 | |
| 22 | | agent | what it does | |
| 23 | |---|---| |
| 24 | | **Claude** | The plain assistant: replies to every DM; in groups only when @mentioned (the default listen mode β the console can widen a bot to also answer its bare name, gated by a cheap "am I being addressed?" classifier). (`runtime.mjs`, identity via `LCHAT_SESSION`/`LCHAT_INVITE`.) | |
| 25 | | **Eleven** | The app guide: same chat loop with a shipped-in persona + app reference (updated with every deploy), and it **DMs each new member a welcome** when they join. (`eleven.mjs`, `LCHAT_ELEVEN_*`.) | |
| 26 | |
| 27 | Each identity is optional β it runs if its session file exists or its invite is |
| 28 | set. Every identity is minted the same way: an agent invite from the admin |
| 29 | console, its `#fragment` passed once as the matching `*_INVITE` env var. |
| 30 | |
| 31 | ## What you provide |
| 32 | |
| 33 | | | | |
| 34 | |---|---| |
| 35 | | `ANTHROPIC_API_KEY` | The only recurring secret + cost. Every DM and every group @mention is one Claude API call billed to your account (bots in the "name" listen mode also spend a fraction-of-a-cent Haiku call per bare-name mention to decide whether to answer β `LCHAT_CLASSIFIER_MODEL` overrides). | |
| 36 | | `LCHAT_BASE` | Your instance URL, e.g. `https://test.example.com`. | |
| 37 | | `LCHAT_INVITE` | **One-time.** The `#fragment` of a `/join/β¦` link minted in the admin console (tick "This is an agent"). Spent after provisioning. | |
| 38 | |
| 39 | Optional: `LCHAT_AGENT_NAME` (default `Claude`), `LCHAT_MODEL` (default |
| 40 | `claude-sonnet-5`; try `claude-opus-4-8` when a bot needs deeper reasoning), `LCHAT_SYSTEM` (system |
| 41 | prompt), `LCHAT_SESSION` (session-file path), `LCHAT_LISTEN` (`mention` = groups |
| 42 | reply only on @mention, the default; `name` = also consider bare-name mentions β |
| 43 | console config overrides per bot). |
| 44 | |
| 45 | ## Run |
| 46 | |
| 47 | ```sh |
| 48 | cd agent |
| 49 | npm install |
| 50 | |
| 51 | # 1. Provision once (creates session.json β the bot's identity; back it up) |
| 52 | LCHAT_BASE=https://test.example.com LCHAT_INVITE='<#fragment from the invite link>' \ |
| 53 | npm run provision |
| 54 | |
| 55 | # 2. Run |
| 56 | export ANTHROPIC_API_KEY=sk-ant-... |
| 57 | LCHAT_BASE=https://test.example.com npm start |
| 58 | ``` |
| 59 | |
| 60 | Then, in the app, add the agent to a group or start a DM with it. It replies to |
| 61 | every message in a DM, and in a group only when @mentioned. An admin can widen |
| 62 | an individual bot's listen mode to `name`, where a message that just uses its |
| 63 | name also gets a reply β but only when a small classifier call agrees the bot is |
| 64 | being spoken *to* rather than *about*, so chatting about a bot never summons it. |
| 65 | |
| 66 | ## Where to run it |
| 67 | |
| 68 | Anywhere. The sidecar is a **pure outbound client** β it dials the instance |
| 69 | (HTTPS/WSS) and the Anthropic API, and nothing ever dials it: no inbound |
| 70 | ports, no webhooks, no public address. The server doesn't know or care where |
| 71 | it runs. |
| 72 | |
| 73 | That choice is the biggest security decision you make about bots, because the |
| 74 | bot's private keys unlock every thread it's a member of: |
| 75 | |
| 76 | - **On the instance box** (the hosted default): convenient, but a compromise of |
| 77 | the chat server also reads the bot's threads. |
| 78 | - **On a machine you trust more** β a home server behind NAT, a spare box, a |
| 79 | Raspberry Pi: now a fully compromised chat server yields only ciphertext, |
| 80 | same as for human members. The attacker can't even route to the keys. |
| 81 | - **Inside a private network with no inbound at all**, egress allowlisted to |
| 82 | the instance + `api.anthropic.com` (or Claude via Bedrock/Vertex private |
| 83 | endpoints for zero public egress). A changelog-recipe bot never calls the model API, |
| 84 | so it needs only the one route to the instance. |
| 85 | |
| 86 | See `docs/bots.md Β§ The sidecar runs anywhere` for the full threat-model |
| 87 | discussion. |
| 88 | |
| 89 | ## Eleven |
| 90 | |
| 91 | **Eleven** needs to see members it shares no thread with (to welcome them), so |
| 92 | vetted bots β agents the admin marked *available to members* β read the full |
| 93 | joined roster; people stay connections-only. On its first run it baselines |
| 94 | silently (existing members are never welcomed retroactively). Its app knowledge |
| 95 | ships in `eleven.mjs`, so it improves with each deploy; console config can |
| 96 | still override the persona per instance. |
| 97 | |
| 98 | (An earlier third identity, the write-only **Announcer**, posted release notes |
| 99 | to groups. It's retired: the server announces its own changelog now β a push to |
| 100 | the space's admins plus the Admin app's update stream. The Go sidecar's |
| 101 | `changelog` recipe rule remains for custom deploy watchers.) |
| 102 | |
| 103 | `node eleven.mjs` runs one standalone welcome tick β handy for tests and |
| 104 | cron-style setups. |
| 105 | |
| 106 | ## Notes & limits |
| 107 | |
| 108 | - **The Claude API sees plaintext.** The lchat server does not β but the reply is |
| 109 | generated by sending decrypted messages to Anthropic. Only a local model would |
| 110 | close that gap. Disclose this where you add the bot. |
| 111 | - **Catches up on reconnect.** On a WebSocket reconnect it backfills messages |
| 112 | missed during the gap (per-thread cursor), so a message sent in the ~1s blip |
| 113 | still gets a reply. A full process restart resets cursors to each thread's |
| 114 | latest, so it won't answer messages sent while the process itself was down (and |
| 115 | never replies to old backlog on first start). |
| 116 | - **No forward secrecy on removal** β same as removing a human member today. |
| 117 | - Keep one sidecar per instance so a compromised host can read only the threads |
| 118 | that bot was added to. |
| 119 | |