docs(ai): record the in-repo ai-bot Synapse appservice (Vojo AI) and the single-file bind-mount / non-root registration gotchas

This commit is contained in:
heaven 2026-05-31 15:40:47 +03:00
parent add6107d66
commit 1385123b55
3 changed files with 19 additions and 2 deletions

View file

@ -54,6 +54,7 @@ src/
└── styles/ # Vanilla-extract global styles (global.css.ts, horseshoe.ts) └── styles/ # Vanilla-extract global styles (global.css.ts, horseshoe.ts)
electron/ # Electron desktop wrapper (see electron.md) electron/ # Electron desktop wrapper (see electron.md)
apps/widget-{telegram,discord,whatsapp}/ # Preact bot-widget apps (see overview.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/`) ## Pages & Routing (`src/app/pages/`)

View file

@ -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/`) - **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 - **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 - **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 ## Branding

View file

@ -119,7 +119,7 @@ in doubt.
| Service | Image | Notes | | Service | Image | Notes |
|---|---|---| |---|---|---|
| `postgres` | `postgres:16` | `POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"` | | `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/<bridge>` directories** (Caddy serves them at the bot widget origins). | | `caddy` | `caddy:2` | Ports 80/443/8448. Bind-mounts `./caddy/Caddyfile`, `./caddy/data`, `./caddy/config`, `./cinny:/var/www/cinny`, **and the three `widgets/<bridge>` 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`. | | `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)** | | `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:<v26.04 bridgev2 tag>` | `./bridges/telegram:/data` | | `telegram-bridge` | `dock.mau.dev/mautrix/telegram:<v26.04 bridgev2 tag>` | `./bridges/telegram:/data` |
| `discord-bridge` | `dock.mau.dev/mautrix/discord:v0.7.5` | `./bridges/discord:/data` (legacy bridge — runtime reports `0.7.6+dev`) | | `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` | | `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) ### Bridge service stanza (template)
@ -203,6 +204,20 @@ configs:
`registration.yaml`. (bridgev2 — telegram/whatsapp — does this correctly.) `registration.yaml`. (bridgev2 — telegram/whatsapp — does this correctly.)
6. **Double puppeting secret prefix `as_token:` is mandatory.** Without it, the bridge 6. **Double puppeting secret prefix `as_token:` is mandatory.** Without it, the bridge
tries to find the deprecated `synapse-shared-secret-auth` Synapse module. 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 ## Refresh procedure