Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/README.md
Claude Agent 8c550a2cde docs(discord-bridge): corectii la partea manuala — View Channel lipsea, Add Bot depasit
- permisiunile de invitatie omiteau *View Channel*, fara de care botul nu vede
  canalul deloc, oricat de permis ar fi in allowlist
- link de invitatie gata calculat (permissions=309237763136), fiindca bifele din
  URL Generator sunt greu de nimerit pe telefon
- *Bot -> Add Bot* nu mai exista: portalul creeaza user-ul bot odata cu aplicatia
- MESSAGE CONTENT INTENT are nevoie de *Save Changes*; fara apasare setarea se pierde
- pasii de copiere ID: pe telefon e apasare lunga, nu click dreapta

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-30 10:55:47 +00:00

326 lines
16 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. **Deschide sectiunea *Bot*.** Portalul creeaza user-ul bot odata cu aplicatia,
deci daca nu vezi un buton *Add Bot* e normal — intri direct in *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.
Dupa comutator apare jos bara **Save Changes** — daca nu o apesi, setarea NU se
salveaza (usor de ratat pe telefon). Reincarca pagina si confirma ca a ramas pornit.
Nu ai nevoie de aprobare de la Discord: verificarea e ceruta abia de la 100 de servere.
(*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: *View Channel*, *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.
Bifele sunt greu de nimerit pe telefon; linkul echivalent, gata calculat
(`APPLICATION_ID` e in *General Information*):
```
https://discord.com/oauth2/authorize?client_id=APPLICATION_ID&scope=bot&permissions=309237763136
```
`309237763136` = exact permisiunile de mai sus. Fara *View Channel* botul nu vede
canalul deloc, oricat de permis ar fi in allowlist.
**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 (pe telefon: apasare lunga) ->
*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 |