Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge
Claude Agent d466f358ce feat(discord-bridge): punte Discord -> Claude Code pe LXC 171
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
2026-08-30 10:44:39 +00:00
..

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

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:

./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

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:

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:

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

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