Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge
Claude Agent feb31eaabe fix(discord-bridge): plafonul de cost e optional (COST_CAP_USD_DAY=off)
Costul raportat de CLI e pretul echivalent la API; pe abonament nu se
factureaza, deci plafonul zilnic oprea puntea fara motiv (5.71 / 5.00 USD).

- limits.parse_cap(): `off`/`none`/`nelimitat`/`0`/gol/gunoi => fara plafon
- stopped()/record_cost() nu mai opresc si nu mai alerteaza cand e dezactivat
- implicit devine `off`; /status arata „(fara plafon)"
- dashboard: /api/status si doctor nu mai crapa pe valoare ne-numerica
- README + ops/env.example explica de ce ramane off pe abonament

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-30 21:02:43 +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 model:opus.

Comenzi

Comanda Ce face
/new Sesiune noua, curata, in firul curent
/new fork:True Sesiune noua care porneste din contextul celei curente
/cd cale:<cale> Schimba directorul de lucru al firului (ex. /cd cale:/workspace/romfastsql)
/model 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:True le opreste
/help Lista de mai sus, in fir

Comenzile sunt application commands (/), inregistrate pe guild-urile din DISCORD_GUILD_IDS la pornirea botului, deci apar in lista de comenzi a Discord. Vechiul prefix ! nu mai executa nimic: botul raspunde doar cu indiciul catre comanda / echivalenta. Orice alt mesaj din canal pleaca la Claude ca prompt, ca inainte.

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 — ~440 MB bucata cu tot cu serverele MCP, pe un container cu istoric de OOM.

DOMENIUL e cgroup-ul serviciului claude-discord.service si numai el. Pe LXC 171 ruleaza permanent sesiuni Claude interactive (tmux, ttyd, agenti) care n-au nicio legatura cu puntea; acelea stau in tmux-spawn-*.scope si NU sunt raportate niciodata, nici macar in rularea seaca. Regula e fail-closed: daca cgroup-ul unui proces nu poate fi citit, procesul e considerat neeligibil — mai bine ratam un orfan decat sa oprim sesiunea cuiva. Fara aceasta limitare, /cleanup force:True dat din Discord si-ar opri propria sesiune impreuna cu tot ce ruleaza omul in tmux.

Inauntrul cgroup-ului, e orfan orice proces neprotejat care nu se leaga de state.json, indiferent cum se numeste — asa raman prinse si serverele MCP pornite de claude (npm exec @playwright/mcp, node ...), care altfel ar scapa fiindca nu se cheama claude. Un fir costa in jur de 440 MB cu tot cu MCP, nu 300.

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 oprirea altui proces. Copiii se opresc inaintea parintilor, altfel s-ar reparenta si ar scapa.


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. 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 si applications.commands -> 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%20applications.commands&permissions=309237763136
    

    309237763136 = exact permisiunile de mai sus. Fara View Channel botul nu vede canalul deloc, oricat de permis ar fi in allowlist. Fara scope-ul applications.commands botul merge, dar inregistrarea comenzilor / esueaza cu 403 Missing Access (scrie in log linkul de reinvitare) si comenzile nu apar in lista. Un bot deja invitat se re-invita cu acelasi link: se adauga doar scope-ul lipsa. 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 off). Pe abonament (Pro/Max) lasa off: costul raportat de CLI e doar pretul echivalent la API, nu o factura. Pune un numar (ex. 5.00) doar daca platesti per token.

  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

Aceleasi lucruri, cu butoane, in dashboard-ul de control (dashboard/README.md) — stare, restart, orfani, confirmari, jurnale:

https://claude-agent.tailf7372d.ts.net/claude    # din tailnet (ca /echo la moltbot)
                                                 # fara login: DASHBOARD_AUTH=off

Procesul e legat de 127.0.0.1:18790; in tailnet il publica tailscale serve. Fara Tailscale: ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171.

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/logs/dashboard.log stdout/stderr al dashboard-ului de control
~/.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. E pretul echivalent la API raportat de CLI: pe abonament nu se factureaza, deci ramane doar o masura de consum.

Plafonul zilnic e optional. COST_CAP_USD_DAY=off (implicit, si 0 sau gol) inseamna fara plafon: cheltuiala se contorizeaza mai departe, dar nimic nu se opreste. Cu un numar pozitiv, la atingerea lui 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.
Comenzile / nu apar in lista din Discord Botul a fost invitat fara scope-ul applications.commands. In log: sync de comenzi slash esuat + linkul de reinvitare. Reinvita botul, apoi systemctl --user restart claude-discord.
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:True. Vezi si systemctl --user show claude-discord -p MemoryCurrent.
„Plafon de cost atins" E limita zilnica, nu o eroare. Pe abonament pune COST_CAP_USD_DAY=off in env si reporneste; altfel ridica valoarea 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. Domeniul lui e strict cgroup-ul serviciului, deci un proces care a iesit complet din cgroup ii scapa — asta e pretul deliberat platit ca sa nu atinga niciodata sesiunile interactive de pe container. 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
commands_slash.py declararea si inregistrarea comenzilor / 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
dashboard/api.py dashboard de control (stare, restart, orfani, confirmari) C
dashboard/index.html, login.html, static/ interfata dashboard-ului C
dashboard/claude-discord-dashboard.service unit systemd pentru dashboard C
INTERFACES.md contractul intre module orchestrator