eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Member bots β€” autonomous, scheduled agents members make by asking
2
3*"Make me a bot that sends me a news summary every morning from NYTimes, RTÉ
4and the Guardian."* This document describes how that works: the design, the
5trust model, and where each piece lives. It is the **Option B** build of
6issue #432 (charter blobs, no new E2EE identity), constructed on top of the
7member-apps machinery (#397) it deliberately mirrors.
8
9## Apps and bots are two axes, not competitors
10
11| | **Member apps** (docs/member-apps.md) | **Member bots** (this) |
12|---|---|---|
13| What it is | an interactive **surface** you open | an autonomous **agent** that messages you |
14| Runs | only while you have it open | on a schedule, unattended |
15| Network | none (sandbox CSP `connect-src 'none'`) | fetches feeds/pages via the sidecar |
16| Execution | sandboxed iframe, on-device | the bot sidecar (holds keys + the model) |
17| Example | habit tracker, poll, whiteboard | morning digest, weekly round-up |
18
19Rule of thumb: **apps are surfaces you open; bots are agents that message
20you.** When something could be either, ask "does it need to act when nobody's
21looking?" β€” yes β†’ bot.
22
23## What a member bot IS: a charter
24
25A member bot is not a new roster member. It is a **charter** β€” a small JSON
26document (persona + schedule + task + where output goes) that the member owns:
27
28```json
29{
30 "v": 1,
31 "name": "morning-news", "title": "Morning News", "glyph": "πŸ“°",
32 "schedule": { "at": "08:00", "tz": "Europe/Dublin" },
33 "task": {
34 "prompt": "Fetch today's headlines from each source; write a short digest…",
35 "tools": ["fetch"],
36 "fetch": { "allow": ["https://rss.nytimes.com/…", "https://www.rte.ie/feeds/…"] },
37 "post": { "card": true }
38 },
39 "thread": "<the bot's dedicated thread>",
40 "owner": "<member uid>", "by": "Paul", "builtBy": "Eleven"
41}
42```
43
44The charter is encrypted under the **instance key** and stored in the server's
45`member_bots` table (`memberbots.go`) exactly like a member app's
46`{source, manifest}` blob: the server learns `(random id, author, owner,
47size, times)` and nothing else β€” not the bot's name, schedule, sources, or
48prompt. Ownership follows the member-app pattern verbatim: an **agent** may
49set `owner_id` to the member it built for; overwrite is author-or-owner;
50delete is author-or-owner-or-admin (moderation without readability).
51
52## Who runs it: the charter host
53
54One identity in the existing sidecar β€” **Eleven** when present, elected in
55`botcmd.go` β€” is the **charter host** (`Runtime.Charters`). It:
56
57- decrypts and validates every stored charter (`internal/bot/charter.go`;
58 fail closed per charter β€” a corrupt row never kills the rest);
59- keeps one **alarm** pending per live charter, using the server's existing
60 scheduled-message queue as the clock (`internal/bot/chartersched.go`): a
61 `bot.tick` payload encrypted under the host's **self-thread** key,
62 delivered by the ordinary sweep (`miniapps.go`). No human thread carries
63 tick traffic, nobody else can ring the alarms, alarms survive restarts
64 (the queue is server-side), and reconcile (on connect + a slow ticker)
65 heals lost or stale ones;
66- on each tick, re-reads the charter (edits/deletes take effect immediately),
67 re-arms the next occurrence *first* (a crash can't silence the bot), then
68 runs the task.
69
70## What a run is: the task engine
71
72`internal/bot/task.go` β€” a bounded agentic loop: system prompt from the
73charter, a closed tool set, execute tool calls, post the final text.
74
75- **fetch** (`internal/bot/fetch.go`): HTTPS GET only, against the charter's
76 URL-prefix allowlist, same private-address blocking as the unfurler,
77 redirects re-checked against the allowlist, bodies capped. A task with no
78 allowlist has no network.
79- **card** (`internal/bot/card.go`): the model may compose one card β€” a small
80 declarative document (heading / text / item / list / kv / divider /
81 one-level section / https-link chips) that renders natively on web
82 (`web/static/cards.js`) and iOS/Mac (`LChatCore/Card.swift` + `CardView`),
83 pinned three ways by `test/event-vectors/cards.json`. Malformed cards
84 bounce back as tool_results the model can fix; unknown cards fail closed at
85 render and the text (the mandatory plain fallback) shows instead.
86- **Budgets**: `max_tool_calls` (default 6, cap 16), fetch bodies truncated
87 for context, one card per run.
88- **Failure backoff**: a run that errors, comes back empty, or reaches *none*
89 of its sources (every fetch failed β€” the all-403 case, where the model
90 writes an apology that would otherwise post as a normal edition every
91 occurrence) is a **failed edition**. The first failure of an outage posts
92 the model's explanation once, with a keep-trying note; further consecutive
93 failures post nothing, and interval schedules stretch the next attempt
94 (2Γ—, 4Γ—, 8×… the interval, capped at a day β€” daily bots keep their day).
95 A successful run clears the state and editions resume by themselves. The
96 counter is in-memory (`chartersched.go`); a sidecar restart forgives,
97 costing at most one extra notice.
98- **Injection story is containment, not detection**: fetched content arrives
99 fenced in tool_result blocks and is framed as data; the model can only call
100 the tools the charter names; output can only go where the charter's thread
101 already points. Worst case is a weird summary in the bot's own thread.
102
103The result posts into the charter's **dedicated thread** β€” a titled group of
104exactly (owner, host) created at birth β€” under the charter's **persona name**
105("Morning News"), so the bot reads as its own presence without being a
106keypair. The plaintext trust note is the same as any AI bot today: the
107sidecar process holds thread plaintext while it works; the *server* still
108never holds a usable key.
109
110**That thread is conversational.** The owner talking there is always talking
111to the host β€” no `@` needed (`triggered`, `runtime.go`, gated on the owner's
112uid so the bot's own persona-named editions never draw a reply) β€” and the
113brain answers with the charter as per-thread system context: the real
114`bot_id`, persona, schedule, sources, and standing instructions
115(`charterHomeNote`, `brain.go`), plus the `create_bot`/`remove_bot` tools. So
116"change it to The Journal", "make it daily at 8", and "remove this bot" all
117work right in the bot's own chat, not just in the DM where it was born.
118Renaming the bot renames its chat too (`renameCharterThread`) β€” but only when
119the persona actually changed, so a group name the member set by hand survives
120an unrelated schedule or source tweak.
121
122## How one is born: the builder, and the dry run as the gate
123
124`internal/bot/botbuild.go`, mirroring `appbuild.go`: the brain's `create_bot`
125tool (offered only on the charter host) is an **executor** β€” the sidecar runs
126the setup on its own goroutine, reporting progress on one action line.
127
1281. Validate the request (slug/title/glyph/schedule/prompt).
1292. **Feed discovery** (`internal/bot/discover.go`): the member names
130 publications ("rte.ie", "the Guardian") β€” never URLs. The builder fetches
131 each site the member named and reads the feed URLs out of its own HTML
132 (`<link rel="alternate" type="…rss/atom…">`). A site with no advertised
133 feed falls back to the page itself; the dry run judges.
1343. **The dry run is the gate**: the task is executed once, immediately. If it
135 fails, comes back empty, or can't read any of its sources, *nothing is
136 created* and the member sees why β€” never a bot that fails silently at 8am,
137 and never one born apologising.
1384. On success: charter stored (owner = requester), dedicated thread created,
139 first alarm armed, and the dry run's output delivered as the bot's **first
140 edition** β€” proof, not promise.
141
142Changing a bot is conversational: ask in the bot's own chat (the host has the
143charter and its id as context there), or ask the builder in the DM where it
144was born (`create_bot` with the `bot_id` from its earlier reply). Ownership is
145checked against the requester in the sidecar, where plaintext lives, either
146way. `remove_bot` retires it: the charter is deleted, alarms cancelled, and
147the bot's thread β€” the member's history β€” stays.
148
149## What the server gained (deliberately little)
150
151One table + three routes (`memberbots.go`), mirroring member apps. Everything
152else rides surfaces the server already had: the schedule queue, self/group
153threads, encrypted thread names, `POST /api/messages`. The tick, the charter,
154the digest, the card β€” all ciphertext.
155
156## Testing
157
158- `internal/bot`: charter/schedule validation + occurrence maths, fetch SSRF
159 + allowlist, feed discovery, card normalization (vector-pinned).
160- Root: `bot_botbuild_test.go` drives the whole pipeline against the real
161 router with a scripted model β€” create_bot β†’ discovery β†’ dry run (with a
162 thinking block, pinning the echo-thinking-intact fix) β†’ stored charter β†’
163 dedicated thread β†’ persona-named first edition with card β†’ armed alarm;
164 and the autonomous loop β€” tick delivered β†’ task run β†’ post β†’ re-arm.
165 `memberbots_test.go` pins the ownership gates.
166- Web: `test/cards.test.mjs` (+ `test/event-vectors/cards.json`); native:
167 `CardVectorTests.swift` reads the same vectors.
168
169## Try it locally (two terminals + a browser)
170
171```sh
172# 1 β€” a local server, seeded and signed in (persistent across restarts):
173tools/lchat-dev-up.mjs # open the printed sign-in link
174
175# 2 β€” the sidecar; its one identity becomes the charter host (the 1m floor
176# lets you watch a bot fire β€” production's floor is 15m):
177ANTHROPIC_API_KEY=sk-ant-… LCHAT_BOT_MIN_EVERY=1m tools/lchat-dev-bot.mjs
178
179# 3 β€” in the browser: οΌ‹ New chat β†’ Helpers β†’ Claude, then ask:
180# "make me a bot that sends me a news summary every 2 minutes
181# from rte.ie and theguardian.com"
182```
183
184Watch: the status line advances (finding the feeds β†’ running a first edition
185β†’ opening its chat), a new **chat named after the bot** appears with the first
186edition (a card over plain text), and further editions arrive on schedule.
187Say "every 2 minutes" to watch it fire; "every morning at 8, Irish time" is
188the real thing. Change or retire it by asking in the same DM. If the first ask
189answers "I don't hold this space's shared key yet", ask again a few seconds
190later β€” the bot nudges your open tab to grant it.
191
192## Deferred (deliberately)
193
194- **The drafting-table panel** (preview beside the chat before creation) β€”
195 the dry-run gate delivers the same safety; the panel is UI polish on top
196 (prototype `ae2143b`/`37dbce0` is the reference).
197- **Integrations** (bots using connected services as tools, read/write-scoped
198 grants, confirm chips) β€” designed in `docs/integrations.md` on the
199 `integrations` prototype branch; stacks on this cleanly.
200- **Governance dials**: write rate limits; an admin oversight panel. (The
201 interval floor is handled: 15 minutes in production, lowered only via
202 `LCHAT_BOT_MIN_EVERY` on tests/demo canaries.)
203- A client surface for listing/managing your bots (today: ask in the bot's
204 own chat, or ask the builder).
205