Comenzile devin application commands inregistrate pe guild (sync instantaneu, spre deosebire de cel global care dureaza ~1h): /new [fork], /cd <cale>, /model <sonnet|opus> cu Choice, /status, /stop, /cleanup [force], /help. - allowlist-ul se aplica identic la interactiuni (check_ids comun, ca sa nu existe a doua implementare care diverge); refuz efemer, fara executie - fiecare comanda face defer() inainte de lucru — altfel Discord marcheaza interactiunea esuata dupa 3s desi comanda a rulat - sync tolerant: la esec (lipsa scope applications.commands) botul porneste normal si logheaza linkul de reinvitare necesar - mesajele obisnuite raman neschimbate, inclusiv steering-ul mid-tur - linkul de invitatie primeste scope=bot%20applications.commands; referintele la ! din ops/ si documentatie trecute pe / Verificat in productie: 7 comenzi inregistrate pe guild, citite inapoi din API. Suita: 296 passed cu discord.py, 293 passed + 3 skipped fara. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
17 KiB
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=6Gpe 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 — 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:
~/.claude-discord/cu drepturi0700, pluslogs/siapprovals/~/.claude-discord/envcu0600, copiat dinops/env.example— nu suprascrie niciodata un env existent- venv in
~/.claude-discord/venv+ dependintele dinrequirements.txt loginctl enable-linger claude— fara asta serviciul de utilizator moare la logout si nu porneste la boot- symlink
~/.config/systemd/user/claude-discord.service->ops/claude-discord.service, apoidaemon-reloadsisystemd-analyze verify - 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.
-
Creeaza aplicatia Discord. https://discord.com/developers/applications -> New Application. E o aplicatie noua, dedicata puntii — nu refolosi aplicatia MoltBot/OpenClaw.
-
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.
-
Ia token-ul. Bot -> Reset Token -> copiaza. Se arata o singura data. Il pui in
~/.claude-discord/env, laDISCORD_TOKEN=. Fisierul e0600si nu e versionat. Daca token-ul ajunge vreodata intr-un commit, reseteaza-l imediat din portal — cine il are poate comanda infrastructura. -
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.)
-
Invita botul intr-un guild PRIVAT al tau. OAuth2 -> URL Generator -> scopes:
botsiapplications.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_IDe in General Information):https://discord.com/oauth2/authorize?client_id=APPLICATION_ID&scope=bot%20applications.commands&permissions=309237763136309237763136= exact permisiunile de mai sus. Fara View Channel botul nu vede canalul deloc, oricat de permis ar fi in allowlist. Fara scope-ulapplications.commandsbotul 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. -
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. -
Pune destinatarul alertelor:
ALERT_RECIPIENT=in acelasi env. -
Verifica plafonul de cost:
COST_CAP_USD_DAY=(implicit5.00). -
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 inalerts.logsi 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. 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:
- 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.
- Confirmare pentru operatiuni ireversibile. Un hook
PreToolUseopreste comanda si posteaza butoane in fir; fara raspuns in fereastra de timp raspunsul e deny (fail-closed). Verificat: a blocat unrm -rf, a asteptat aprobarea externa 20s si a permis apoi executia, fara timeout. - Wrapper
infracu 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
bypassPermissionsjumatate 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.
/cleanupnu e infailibil. Prinde proceseclaudeneinregistrate si copii reparentati la init ramasi in cgroup. Un proces care a iesit din cgroup si nu arata aclaude(unpython -m http.serverdesprins complet, de exemplu) ii scapa. ListaNEVER_KILLdincleanup.pyprotejeaza 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 |
INTERFACES.md |
contractul intre module | orchestrator |