From 1385123b552f5a4983b2a388bea02ed090c51ddc Mon Sep 17 00:00:00 2001 From: heaven Date: Sun, 31 May 2026 15:40:47 +0300 Subject: [PATCH] docs(ai): record the in-repo ai-bot Synapse appservice (Vojo AI) and the single-file bind-mount / non-root registration gotchas --- docs/ai/architecture.md | 1 + docs/ai/overview.md | 3 ++- docs/ai/server-side.md | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md index 4eb64ea2..a4cd9e2d 100644 --- a/docs/ai/architecture.md +++ b/docs/ai/architecture.md @@ -54,6 +54,7 @@ src/ └── styles/ # Vanilla-extract global styles (global.css.ts, horseshoe.ts) electron/ # Electron desktop wrapper (see electron.md) apps/widget-{telegram,discord,whatsapp}/ # Preact bot-widget apps (see overview.md) +apps/ai-bot/ # Go Synapse appservice — "Vojo AI" (@ai), xAI-Grok backend (server-side, NOT client; see its README + server-side.md) ``` ## Pages & Routing (`src/app/pages/`) diff --git a/docs/ai/overview.md b/docs/ai/overview.md index d785174f..745de32b 100644 --- a/docs/ai/overview.md +++ b/docs/ai/overview.md @@ -23,8 +23,9 @@ Value proposition: "Telegram works without VPN" — the server runs a mautrix-te - **Synapse modules**: in-tree `username_blocklist.py` bind-mounted into the Synapse image (`synapse/modules/`) - **Client**: this repo — built and deployed as static files via Caddy - **Bot widgets**: three Preact apps in `apps/widget-{telegram,discord,whatsapp}/`, built independently and deployed to `~/vojo/widgets/{telegram,discord,whatsapp}/` for BotShell to embed +- **AI bot ("Vojo AI", `@ai:vojo.chat`)**: a Go **Synapse appservice** in [`apps/ai-bot/`](../../apps/ai-bot/) (xAI-Grok backend; answers `@`-mentions in groups + all 1:1 messages). Built locally + shipped via `docker save`/`load`, deployed to `~/vojo/ai-bot/`. v1 is **backend-only — users add it by inviting `@ai` into any room manually**; the in-app "Bots"-tab widget + "Add to chat" picker (plan Parts B/C) are deferred. Branded **Vojo AI** with a generic icon — "Grok" is only the factual "powered by" attribution + the model id (xAI trademark). 152-ФЗ / transborder-transfer legal gating is a pre-launch item (see `docs/plans/grok_bot.md` §6). See `server-side.md` + `apps/ai-bot/README.md`. -Server filesystem under `~/vojo/`: `caddy/`, `synapse/`, `postgres/`, `coturn/`, `livekit/`, `sygnal/`, `prometheus/`, `grafana/`, `bridges/{telegram,discord,whatsapp}/`, `widgets/{telegram,discord,whatsapp}/`, `cinny/`, `docker-compose.yml`. Caddy serves the client at `https://vojo.chat` with the host's `cinny/` directory bind-mounted into the container as `/var/www/cinny`. +Server filesystem under `~/vojo/`: `caddy/`, `synapse/`, `postgres/`, `coturn/`, `livekit/`, `sygnal/`, `prometheus/`, `grafana/`, `bridges/{telegram,discord,whatsapp}/`, `widgets/{telegram,discord,whatsapp}/`, `ai-bot/`, `cinny/`, `docker-compose.yml`. Caddy serves the client at `https://vojo.chat` with the host's `cinny/` directory bind-mounted into the container as `/var/www/cinny`. ## Branding diff --git a/docs/ai/server-side.md b/docs/ai/server-side.md index bbd50dc9..49144e2e 100644 --- a/docs/ai/server-side.md +++ b/docs/ai/server-side.md @@ -119,7 +119,7 @@ in doubt. | Service | Image | Notes | |---|---|---| | `postgres` | `postgres:16` | `POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"` | -| `synapse` | `matrixdotorg/synapse:latest` | Bind-mounts: `./synapse:/data` plus three module mounts (`username_blocklist.py`, `blocked_usernames.txt`, `blocked_patterns.txt` → `/usr/local/lib/python3.13/site-packages/`), and four registration mounts (`bridges/telegram/registration.yaml`, `synapse/doublepuppet.yaml`, `bridges/discord/registration.yaml`, `bridges/whatsapp/registration.yaml`) into `/data/*-registration.yaml`. Exposes 8008. | +| `synapse` | `matrixdotorg/synapse:latest` | Bind-mounts: `./synapse:/data` plus three module mounts (`username_blocklist.py`, `blocked_usernames.txt`, `blocked_patterns.txt` → `/usr/local/lib/python3.13/site-packages/`), and **five** registration mounts (`bridges/telegram/registration.yaml`, `synapse/doublepuppet.yaml`, `bridges/discord/registration.yaml`, `bridges/whatsapp/registration.yaml`, `ai-bot/registration.yaml`) into `/data/*-registration.yaml`. Exposes 8008. | | `caddy` | `caddy:2` | Ports 80/443/8448. Bind-mounts `./caddy/Caddyfile`, `./caddy/data`, `./caddy/config`, `./cinny:/var/www/cinny`, **and the three `widgets/` directories** (Caddy serves them at the bot widget origins). | | `coturn` | `coturn/coturn:latest` | `network_mode: host`; `./coturn/turnserver.conf` mounted read-only at `/etc/coturn/turnserver.conf`. | | `livekit` | LiveKit server image | Backed by `./livekit/livekit.yaml` + `./livekit/secrets`. Element Call backend. **(env / image tag not on hand — read the file)** | @@ -130,6 +130,7 @@ in doubt. | `telegram-bridge` | `dock.mau.dev/mautrix/telegram:` | `./bridges/telegram:/data` | | `discord-bridge` | `dock.mau.dev/mautrix/discord:v0.7.5` | `./bridges/discord:/data` (legacy bridge — runtime reports `0.7.6+dev`) | | `whatsapp-bridge` | `dock.mau.dev/mautrix/whatsapp:v0.12.4` | `./bridges/whatsapp:/data` | +| `ai-bot` | `ai-bot:custom` (built locally from [`apps/ai-bot/`](../../apps/ai-bot/), shipped via `docker save \| ssh docker load` — VS Code task **Deploy AI bot**) | **Vojo AI** = `@ai:vojo.chat`, an xAI-Grok-backed **application service** (NOT a normal bot user). Answers `@`-mentions in groups + everything in 1:1s. Mounts `./ai-bot:/data` (owned **uid 65532**, distroless nonroot) holding `registration.yaml` (self-generated, `generate-registration`), `state/` (SQLite spend ledger + txn dedup) and `secrets/xai_api_key`. Push port `:8009` (registration `url: http://ai-bot:8009`). Secrets via env/`*_FILE`; `as_token`/`hs_token` read from `registration.yaml` (no rotation). See [`apps/ai-bot/README.md`](../../apps/ai-bot/README.md). | ### Bridge service stanza (template) @@ -203,6 +204,20 @@ configs: `registration.yaml`. (bridgev2 — telegram/whatsapp — does this correctly.) 6. **Double puppeting secret prefix `as_token:` is mandatory.** Without it, the bridge tries to find the deprecated `synapse-shared-secret-auth` Synapse module. +7. **Single-file bind-mount + `depends_on` = phantom directory.** Docker creates the + *source* of a single-file bind-mount as an **empty directory** if it doesn't exist when + the container starts. This bit the `ai-bot` bring-up: Synapse `depends_on`-started while + `ai-bot/registration.yaml` didn't exist yet → Docker made it a dir → Synapse crashed + `IsADirectoryError`, and `generate-registration` then saw the dir and refused to write. + Rule: **generate the file BEFORE the mounting container starts.** Generate the bot's + registration with `docker compose run --rm --no-deps ai-bot generate-registration` + (`--no-deps` so Synapse doesn't start and recreate the dir). Bridges avoid this because + each bridge writes its own `registration.yaml` into its `/data` before Synapse mounts it. +8. **Registration files must be world-readable (`644`).** The Synapse container runs + **non-root**, so a `0600` registration owned by another uid yields `PermissionError`. + `ai-bot`'s `generate-registration` writes `0644` for this reason (token secrecy relies on + host access control on the single-tenant VPS, not the file mode). The xAI key stays a + separate `0600`-ish secret file the bot reads as its own uid. ## Refresh procedure