Implementeaza planul claude-master-plan-discord-bridge-20260830 (15 taskuri, 3 lane-uri paralele) — un bot subtire discord.py peste CLI-ul `claude`, cu proces persistent per fir alimentat pe stdin cu --input-format stream-json. Nucleu: runner (proces persistent + reaper 20min + respawn --resume), stream (parser tolerant), session_store (scriere atomica, lock per fir, detectare PID reuse, recovery), limits (max 4 procese, timeout tur, rate per user, plafon cost pe zi), render (un loop de editare per canal, interval adaptiv). Adaptor: allowlist guild/canal/user fail-closed cu respingerea webhook-urilor, comenzi !new/!cd/!model/!status/!stop/!cleanup, cost si model in subsolul fiecarui raspuns. Mesajul sosit in timpul unui tur devine steering, nu tur nou. Securitate: hook PreToolUse fail-closed care cere confirmare in Discord pentru operatiuni ireversibile, wrapper `infra` cu lista explicita de hosturi. Deny rules raman strat cosmetic, nu bariera (verificat: /usr/bin/ssh trece pe langa). Ops: alerte email pe conventia repo-ului, !cleanup pentru orfani, unit systemd user cu KillMode=control-group si limite de memorie, install.sh idempotent. Verificat: 275 teste fara retea/Discord/API (10.8s), identic cu si fara discord.py instalat; e2e pe CLI real confirma steering-ul mid-tur (mesaj la 6s intr-un tool call de 25s schimba raspunsul final). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
312 lines
15 KiB
Markdown
312 lines
15 KiB
Markdown
# Punte Discord -> Claude Code (LXC 171)
|
|
|
|
Un bot Discord subtire care duce mesajele dintr-un guild privat catre CLI-ul `claude`
|
|
care ruleaza pe containerul de dezvoltare **LXC 171 (claude-agent, 10.0.20.171)**, si
|
|
aduce raspunsurile inapoi. Practic: acelasi Claude Code cu care lucrezi in terminal,
|
|
comandat de pe telefon.
|
|
|
|
Nu e un chatbot separat. Nu are memorie proprie, nu are baza de date proprie: sesiunile
|
|
sunt chiar sesiunile Claude Code din `~/.claude/projects/`, iar un fir de Discord este
|
|
o sesiune.
|
|
|
|
> Nu confunda cu **MoltBot** (LXC 110) sau cu OpenClaw — acelea sunt alti agenti, cu
|
|
> alt scop. Puntea asta ruleaza pe masina de dezvoltare si are accesul ei.
|
|
|
|
---
|
|
|
|
## Arhitectura
|
|
|
|
```
|
|
Discord (guild privat)
|
|
| on_message
|
|
v
|
|
bot.py -- allowlist (guild / canal / utilizator) [doar adaptor Discord]
|
|
|
|
|
+-- session_store.py state.json {thread_id: {sid, cwd, model, inflight, pid}}
|
|
+-- runner.py proces persistent per fir, alimentat pe stdin
|
|
+-- stream.py parser tolerant de JSONL
|
|
+-- render.py chunker + un loop de editare per canal
|
|
+-- limits.py max procese, timeout tur, rate limit, plafon de cost
|
|
+-- security/ hook PreToolUse (confirmari) + wrapper `infra`
|
|
+-- alerts.py alerte email [ops]
|
|
+-- cleanup.py procese lasate in urma (`!cleanup`) [ops]
|
|
|
|
|
v
|
|
claude -p --input-format stream-json --output-format stream-json --verbose
|
|
--resume <sid> --permission-mode bypassPermissions
|
|
--settings ~/.claude-discord/bot-settings.json --model sonnet --autocompact auto
|
|
```
|
|
|
|
Cateva alegeri care nu se vad din diagrama:
|
|
|
|
- **Proces persistent per fir**, nu unul per mesaj. Asta permite *steering* la mijlocul
|
|
turului: un mesaj trimis in timp ce Claude lucreaza ajunge la el si schimba raspunsul
|
|
(verificat: mesaj la 8s intr-un tur de 34.5s). Reaper la 20 min de inactivitate;
|
|
repornirea se face cu `--resume <sid>`, deci firul nu-si pierde contextul.
|
|
- **Un proces `claude` = ~406 MB RSS** (masurat). De aici toate limitele: maxim 4 procese
|
|
vii, `MemoryMax=6G` pe unit, si comanda `!cleanup`.
|
|
- **Model implicit `sonnet`.** Un tur banal pe opus a costat $0.1547 (masurat), deci
|
|
opus e optional, per fir, prin `!model opus`.
|
|
|
|
---
|
|
|
|
## Comenzi
|
|
|
|
| Comanda | Ce face |
|
|
|---------|---------|
|
|
| `!new` | Sesiune noua, curata, in firul curent |
|
|
| `!new --fork` | Sesiune noua care porneste din contextul celei curente |
|
|
| `!cd <cale>` | Schimba directorul de lucru al firului (ex. `!cd /workspace/romfastsql`) |
|
|
| `!model <sonnet\|opus>` | Schimba modelul pentru firul curent |
|
|
| `!status` | Sesiune, director, model, cost cumulat, proces viu, ultimele linii de stderr |
|
|
| `!stop` | Opreste turul in desfasurare din firul curent |
|
|
| `!cleanup` | Listeaza procesele lasate in urma (rulare seaca). `!cleanup --force` le opreste |
|
|
|
|
Un fir de Discord = o sesiune Claude. Canalul principal are si el sesiunea lui, cea
|
|
implicita. Subsolul fiecarui raspuns arata modelul, durata si costul.
|
|
|
|
### Despre `!cleanup`
|
|
|
|
`KillMode=control-group` opreste arborele serviciului la restart, dar **nu prinde ce s-a
|
|
desprins**: un server pornit cu `&` intr-un tur, un `nohup`, un job lung reparentat la
|
|
init. Alea raman si se aduna — 406 MB bucata, pe un container cu istoric de OOM.
|
|
|
|
`!cleanup` cauta doua feluri de resturi: procese `claude` care nu apar in `state.json`,
|
|
si copii reparentati la init ramasi in cgroup-ul serviciului. **Ruleaza sec (dry-run) in
|
|
mod implicit** — intai vezi lista, apoi decizi. Ce e inregistrat in `state.json` si toti
|
|
descendentii acelor procese (adica turul care ruleaza chiar acum) nu sunt niciodata
|
|
atinse, iar potrivirea se face si pe `pid_start_time`, ca un PID reciclat sa nu duca la
|
|
omorarea altui proces.
|
|
|
|
---
|
|
|
|
## Instalare
|
|
|
|
### Partea automata
|
|
|
|
```bash
|
|
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
|
|
./ops/install.sh
|
|
```
|
|
|
|
Scriptul e idempotent (poti sa-l rulezi de cate ori vrei) si face:
|
|
|
|
1. `~/.claude-discord/` cu drepturi `0700`, plus `logs/` si `approvals/`
|
|
2. `~/.claude-discord/env` cu `0600`, copiat din `ops/env.example` — **nu suprascrie
|
|
niciodata un env existent**
|
|
3. venv in `~/.claude-discord/venv` + dependintele din `requirements.txt`
|
|
4. `loginctl enable-linger claude` — fara asta serviciul de utilizator moare la logout
|
|
si nu porneste la boot
|
|
5. symlink `~/.config/systemd/user/claude-discord.service` -> `ops/claude-discord.service`,
|
|
apoi `daemon-reload` si `systemd-analyze verify`
|
|
6. intrare de crontab pentru `logrotate` (zilnic, 04:10)
|
|
|
|
Scriptul **nu porneste** serviciul. Dupa ce completezi env-ul:
|
|
|
|
```bash
|
|
./ops/install.sh --start
|
|
```
|
|
|
|
### Partea manuala (o faci tu, o singura data)
|
|
|
|
Nu se poate automatiza: cere un om logat in Discord.
|
|
|
|
1. **Creeaza aplicatia Discord.** https://discord.com/developers/applications ->
|
|
*New Application*. E o aplicatie **noua, dedicata** puntii — nu refolosi aplicatia
|
|
MoltBot/OpenClaw.
|
|
2. **Adauga botul.** In aplicatie -> *Bot* -> *Add Bot*.
|
|
3. **Ia token-ul.** *Bot* -> *Reset Token* -> copiaza. **Se arata o singura data.**
|
|
Il pui in `~/.claude-discord/env`, la `DISCORD_TOKEN=`. Fisierul e `0600` si nu e
|
|
versionat. Daca token-ul ajunge vreodata intr-un commit, reseteaza-l imediat din
|
|
portal — cine il are poate comanda infrastructura.
|
|
4. **Activeaza intents.** *Bot* -> *Privileged Gateway Intents* -> porneste
|
|
**MESSAGE CONTENT INTENT**. Fara el botul primeste mesajele goale si nu face nimic.
|
|
(*Server Members* si *Presence* nu sunt necesare — lasa-le oprite.)
|
|
5. **Invita botul intr-un guild PRIVAT** al tau. *OAuth2* -> *URL Generator* ->
|
|
scopes: `bot` -> permisiuni: *Send Messages*, *Read Message History*,
|
|
*Create Public Threads*, *Send Messages in Threads*, *Attach Files*,
|
|
*Embed Links*, *Add Reactions*. Deschide URL-ul generat si alege serverul.
|
|
**Nu-l invita intr-un server cu alti oameni** — cine scrie in canalul permis
|
|
comanda direct containerul.
|
|
6. **Ia ID-urile pentru allowlist.** In Discord: *Settings* -> *Advanced* ->
|
|
**Developer Mode** pornit. Apoi click dreapta -> *Copy Server ID* /
|
|
*Copy Channel ID* / *Copy User ID*. Le pui in `~/.claude-discord/env`:
|
|
`DISCORD_GUILD_IDS`, `DISCORD_CHANNEL_IDS`, `DISCORD_USER_IDS` (separate prin virgula).
|
|
**Allowlist gol = nimic permis** (fail-closed). Mesajele de la webhook-uri si de la
|
|
alti boti sunt ignorate din principiu.
|
|
7. **Pune destinatarul alertelor**: `ALERT_RECIPIENT=` in acelasi env.
|
|
8. **Verifica plafonul de cost**: `COST_CAP_USD_DAY=` (implicit `5.00`).
|
|
9. Abia acum: `./ops/install.sh --start`.
|
|
|
|
---
|
|
|
|
## Operare
|
|
|
|
### Unde te uiti
|
|
|
|
```bash
|
|
systemctl --user status claude-discord # e viu?
|
|
journalctl --user -u claude-discord -n 200 # ce a facut ultima data
|
|
tail -f ~/.claude-discord/logs/bot.log # logul aplicatiei
|
|
tail -f ~/.claude-discord/logs/alerts.log # ce alerte s-au trimis / au esuat
|
|
systemctl --user restart claude-discord # repornire
|
|
```
|
|
|
|
| Fisier | Ce e |
|
|
|--------|------|
|
|
| `~/.claude-discord/env` | token + allowlist + limite (0600) |
|
|
| `~/.claude-discord/state.json` | sesiuni, directoare, pid-uri, cost |
|
|
| `~/.claude-discord/logs/bot.log` | stdout/stderr al botului (rotit zilnic, 14 zile) |
|
|
| `~/.claude-discord/logs/alerts.log` | jurnalul alertelor |
|
|
| `~/.claude-discord/alerts-dedup.json` | fereastra de dedup a alertelor |
|
|
| `~/.claude-discord/approvals/` | cereri de confirmare intre hook si bot |
|
|
|
|
### Cost
|
|
|
|
Costul se vede in trei locuri: in subsolul fiecarui raspuns (turul curent + cumulat pe
|
|
fir), in `!status`, si in `state.json` la cheia `cost`. La atingerea plafonului zilnic
|
|
(`COST_CAP_USD_DAY`) botul nu mai accepta tururi noi si trimite email. Plafonul se
|
|
reseteaza la schimbarea zilei.
|
|
|
|
### Alerte pe email
|
|
|
|
`alerts.py` urmeaza tiparul deja folosit in repo (vezi
|
|
`proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh`):
|
|
|
|
```
|
|
mail -s "[LEVEL] subiect" "$ALERT_RECIPIENT" # LEVEL: INFO | WARN | CRITICAL
|
|
```
|
|
|
|
Se trimite alerta pentru: proces mort neasteptat, crash loop, plafon de cost atins,
|
|
`state.json` corupt, orfani detectati la sweep.
|
|
|
|
Doua garantii care conteaza:
|
|
|
|
- **`alert()` nu arunca niciodata exceptii.** O alerta esuata nu are voie sa doboare
|
|
botul; orice eroare ajunge in `alerts.log` si atat.
|
|
- **Dedup 1h pe `dedup_key`**, persistat pe disc. Fara el, un crash loop ar trimite
|
|
sute de emailuri identice.
|
|
|
|
**Dependinta:** binarul `mail`. Pe LXC 171 e instalat pachetul **`bsd-mailx`**
|
|
(`/usr/bin/mail`), iar transportul e **postfix**, deja prezent (`/usr/sbin/sendmail`).
|
|
Pe o masina unde lipseste:
|
|
|
|
```bash
|
|
sudo apt-get install -y bsd-mailx
|
|
```
|
|
|
|
Daca `mail` lipseste, alertele **nu se pierd**: se degradeaza la scriere in
|
|
`~/.claude-discord/logs/alerts.log`, cu tot cu corpul mesajului, si `install.sh` te
|
|
avertizeaza la instalare. Dar nimeni nu mai primeste nimic pe email — deci trateaza
|
|
lipsa lui ca pe o defectiune, nu ca pe o optiune.
|
|
|
|
Test manual, fara sa pornesti botul:
|
|
|
|
```bash
|
|
ALERT_RECIPIENT=tu@romfast.ro python3 alerts.py INFO "test punte" "corp de test"
|
|
tail -2 ~/.claude-discord/logs/alerts.log
|
|
```
|
|
|
|
### Cand pica
|
|
|
|
| Simptom | Ce faci |
|
|
|---------|---------|
|
|
| Botul nu raspunde deloc in Discord | `systemctl --user status claude-discord`. Daca e `failed`, `journalctl --user -u claude-discord -n 100`. Cauza #1: token invalid sau **MESSAGE CONTENT INTENT** oprit. |
|
|
| Botul e viu dar ignora mesajele | Allowlist. Verifica `DISCORD_GUILD_IDS` / `DISCORD_CHANNEL_IDS` / `DISCORD_USER_IDS` din env. Respingerea e **tacuta**, intentionat. |
|
|
| Unitul se invarte in restart | Dupa 5 porniri esuate in 300s systemd renunta si lasa unitul `failed` (e voit). Repara, apoi `systemctl --user reset-failed claude-discord && systemctl --user start claude-discord`. |
|
|
| Firul e blocat pe hourglass | Botul a fost restartat la mijlocul unui tur. Turul **nu** se reia automat (risc de dubla executie sub `bypassPermissions`); sweep-ul de la pornire pune un avertisment in fir. Trimite mesajul din nou. |
|
|
| Memoria containerului creste | `!cleanup` (sec), apoi `!cleanup --force`. Vezi si `systemctl --user show claude-discord -p MemoryCurrent`. |
|
|
| „Plafon de cost atins" | E limita zilnica, nu o eroare. Ridica `COST_CAP_USD_DAY` in env si reporneste, sau asteapta ziua urmatoare. |
|
|
| Nu vin emailuri de alerta | `command -v mail`; `mailq`; `tail ~/.claude-discord/logs/alerts.log`. Un `NESENT` in log iti spune exact de ce. |
|
|
| Dupa reboot serviciul nu porneste | `loginctl show-user claude -p Linger` trebuie sa fie `yes`. Daca nu: `sudo loginctl enable-linger claude`. |
|
|
|
|
### Teste
|
|
|
|
```bash
|
|
cd /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge
|
|
python3 -m pytest -q # suita rapida, fara retea si fara Discord
|
|
python3 -m pytest -m e2e # testele care ating CLI-ul real (lente)
|
|
```
|
|
|
|
Testele de ops (`tests/test_alerts.py`, `tests/test_cleanup.py`) nu trimit email real si
|
|
nu omoara procese reale: folosesc un `mail` fals si copii de `sleep` pe care le pornesc
|
|
si le opresc ele insele.
|
|
|
|
---
|
|
|
|
## Securitate — ce e si ce nu e
|
|
|
|
Puntea ruleaza ca utilizatorul `claude`, cu `--permission-mode bypassPermissions`, si are
|
|
**exact accesul pe care il are omul in terminal**: `/workspace`, cheile SSH, nodurile
|
|
Proxmox, LXC-urile, VM-urile. Asta e **functionalitate ceruta**, nu scapare — puntea
|
|
exista tocmai ca sa poti administra infrastructura de pe telefon.
|
|
|
|
Ce apara efectiv:
|
|
|
|
1. **Control de acces pe canal.** Guild + canal + utilizator pe allowlist, gol = nimic
|
|
permis. Webhook-urile si botii sunt respinsi. Contul tau de Discord devine, practic,
|
|
o cheie de infrastructura — pune-i 2FA.
|
|
2. **Confirmare pentru operatiuni ireversibile.** Un hook `PreToolUse` opreste comanda si
|
|
posteaza butoane in fir; fara raspuns in fereastra de timp raspunsul e **deny**
|
|
(fail-closed). Verificat: a blocat un `rm -rf`, a asteptat aprobarea externa 20s si a
|
|
permis apoi executia, fara timeout.
|
|
3. **Wrapper `infra`** cu lista explicita de hosturi + token Proxmox cu ACL.
|
|
|
|
Ce **nu** apara: regulile `deny` din settings. Sub `bypassPermissions` ele sunt un strat
|
|
cosmetic — verificat, `/usr/bin/ssh -V` si `bash -c "ssh -V"` trec pe langa ele. Nu te
|
|
baza pe ele ca pe o bariera.
|
|
|
|
---
|
|
|
|
## Limitari cunoscute (asumate)
|
|
|
|
- **Nu exista jurnal de audit independent.** Stratul de audit append-only pe branch
|
|
dedicat a fost considerat si **respins constient**. Consecinta, asumata: la o problema
|
|
— o comanda distructiva care a trecut, o modificare pe care nimeni nu si-o aminteste —
|
|
**nu exista o inregistrare independenta care sa spuna ce s-a intamplat, cand si pe ce
|
|
host**. Ce ramane sunt loguri care pot fi sterse de chiar procesul care le scrie:
|
|
`bot.log`, `journalctl`, jurnalele de sesiune din `~/.claude/projects/` si istoricul
|
|
git al repo-urilor atinse. Daca vreodata conteaza „cine si ce", asta e golul de
|
|
acoperit primul.
|
|
- **Fara reluare automata a turului pierdut.** Un restart la mijlocul unui tur pierde
|
|
turul; nu se reia singur, fiindca sub `bypassPermissions` jumatate din comenzi sunt
|
|
deja executate si o reluare le-ar rula a doua oara.
|
|
- **Rate limit-ul Discord e o degradare tacuta.** Intervalul de editare se adapteaza
|
|
(1s -> 5s), dar cand Discord franeaza nu apare niciun mesaj: raspunsul doar apare mai
|
|
incet. E singura cale fara test din analiza modurilor de esec — acceptata, fiindca
|
|
esecul e intarziere, nu pierdere.
|
|
- **`!cleanup` nu e infailibil.** Prinde procese `claude` neinregistrate si copii
|
|
reparentati la init ramasi in cgroup. Un proces care a iesit din cgroup *si* nu arata
|
|
a `claude` (un `python -m http.server` desprins complet, de exemplu) ii scapa.
|
|
Lista `NEVER_KILL` din `cleanup.py` protejeaza infrastructura sesiunii (systemd, sshd,
|
|
tmux, code-server) — deci un proces cu un asemenea nume in linia de comanda nu va fi
|
|
oprit niciodata, chiar daca e orfan.
|
|
- **Fara voce, imagini sau atasamente** catre Claude in v1.
|
|
- **Fara dashboard web** — exista deja pe MoltBot, puntea nu-l duplica.
|
|
- **Un singur container.** Daca LXC 171 e oprit, puntea e oprita. Nu are redundanta si
|
|
nu e in HA.
|
|
|
|
---
|
|
|
|
## Fisiere
|
|
|
|
| Fisier | Ce e | Lane |
|
|
|--------|------|------|
|
|
| `bot.py` | adaptorul Discord: allowlist, comenzi, butoane | A |
|
|
| `session_store.py` | `state.json`: scriere atomica, lock per fir, PID reuse | A |
|
|
| `runner.py` | proces persistent per fir, stdin JSONL, reaper | A |
|
|
| `stream.py` | parser tolerant de stream JSONL | A |
|
|
| `render.py` | chunker + loop de editare per canal | A |
|
|
| `limits.py` | max procese, timeout, rate limit, plafon de cost | A |
|
|
| `config.py` | citeste `~/.claude-discord/env` | A |
|
|
| `security/confirm_hook.py` | hook `PreToolUse`, fail-closed | B |
|
|
| `security/approvals.py` | canal de aprobari hook <-> bot | B |
|
|
| `security/infra` | wrapper cu lista de hosturi permise | B |
|
|
| `alerts.py` | alerte email, dedup 1h, nu arunca niciodata | C |
|
|
| `cleanup.py` | `!cleanup`: orfani, dry-run implicit | C |
|
|
| `ops/claude-discord.service` | unit systemd de utilizator | C |
|
|
| `ops/install.sh` | instalare idempotenta | C |
|
|
| `ops/env.example` | sablon de configurare | C |
|
|
| `ops/logrotate.conf` | rotatia logurilor | C |
|
|
| `INTERFACES.md` | contractul intre module | orchestrator |
|