Doua defecte, al doilea gasit la verificarea primului.
1. Procesul `claude` al unui fir isi porneste serverele MCP (npm/sh/node pentru
playwright), care nu se numesc `claude` si scapau cautarii. Masurat pe firul viu:
claude 300 MB + MCP 137 MB = ~440 MB per fir, nu 300. find_orphans merge acum pe
arbore, iar kill_orphans opreste copiii inaintea parintilor (altfel se reparenteaza
si scapa).
2. PERICULOS: definitia initiala a orfanului ("orice `claude` absent din state.json")
prindea sesiunile Claude interactive de pe container. Rularea seaca propunea 25 de
procese / ~3864 MB — sesiunile de lucru din tmux, inclusiv cea din care ar fi fost
data comanda. `/cleanup force:True` din Discord si-ar fi omorat propria sesiune.
Apartenenta la cgroup-ul `claude-discord.service` devine conditie necesara pentru
toate familiile, fail-closed la cgroup necitibil. Dupa fix: zero procese raportate.
Test de regresie pe instantaneul real (doua sesiuni in tmux-spawn-*.scope, una in
claude-discord.service): se raporteaza doar a treia. Verificat prin mutant ca testul
musca — cu filtrul scos pica 3 teste.
3. INTERFACES.md cerea pid_start_time in ticks, dar session_store scrie secunde (si
state.json viu contine secunde). Contractul era imprecis, nu codul: un consumator
care compara ticks nu s-ar potrivi niciodata, iar cum acea comparatie protejeaza
turul in desfasurare, esecul ar fi fost tacut si ar fi facut eligibil exact ce
trebuia protejat. Documentat, cu avertismentul explicit.
Bug colateral prins de agent: raportul cu rezultate de omorare ajungea la 2289 caractere,
peste limita Discord — mesajul ar fi fost respins exact la `/cleanup force:True`. Buget
de caractere adaugat.
Suita: 322 passed cu discord.py, 319 passed + 3 skipped fara. Rulare seaca pe container: curat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
349 lines
18 KiB
Markdown
349 lines
18 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 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
|
|
|
|
```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` **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 `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. |
|
|
| 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
|
|
|
|
```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.** 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 |
|
|
| `INTERFACES.md` | contractul intre module | orchestrator |
|