Regulile deny au precedenta peste --permission-mode bypassPermissions si opresc turul INAINTE de hook-ul PreToolUse, deci puntea nu putea ajunge la niciun host prin ssh (simptom: agentul raporta "permisiunea a fost respinsa" pentru ssh moltbot@10.0.20.173, desi cheia, DNS-ul si reteaua erau in regula). Nota din README care sustinea ca deny e doar strat cosmetic era gresita: verificarea de atunci folosise /usr/bin/ssh -V si bash -c "ssh -V", care ocolesc potrivirea pe prefix, nu forma normala ssh host cmd. Bariera reala ramane confirm_hook.py + wrapper-ul infra. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
Securitatea puntii Discord -> Claude Code (Lane B)
Botul ruleaza CLI-ul claude cu --permission-mode bypassPermissions. Asta e o decizie
deliberata: accesul la nodurile Proxmox, la LXC-uri si la VM-uri este functionalitate ceruta,
nu accident. Atentie la regulile deny din settings: ele au precedenta peste bypassPermissions si
opresc turul inainte de hook. O verificare mai veche a concluzionat gresit ca sunt doar strat
cosmetic (se testase /usr/bin/ssh -V si bash -c "ssh -V", care ocolesc potrivirea pe prefix);
un ssh host cmd scris normal e insa refuzat sec. De aceea Bash(ssh:*) si Bash(scp:*) au fost
scoase din bot-settings.json pe 2026-08-30 — cu ele acolo, puntea nu putea ajunge la niciun host
(simptom: „permisiunea a fost respinsa" la ssh moltbot@10.0.20.173, desi cheia si reteaua erau in
regula). Nu le pune la loc: accesul SSH la infrastructura e functionalitate ceruta, iar bariera
reala e hook-ul de mai jos.
Straturile reale sunt:
| Strat | Unde | Ce face |
|---|---|---|
| 1. Control de acces | bot.py (Lane A) |
cine poate scrie in canal |
| 2. Confirmare pentru operatiuni ireversibile | confirm_hook.py + approvals.py |
hook PreToolUse care blocheaza si asteapta un buton in Discord |
| 4. Poarta spre infrastructura | infra + token Proxmox cu ACL |
hosturi dintr-o lista explicita, fiecare apel jurnalizat |
Stratul 3 (audit append-only pe branch dedicat) a fost respins constient de utilizator.
1. Fluxul de confirmare
claude (bypassPermissions)
| PreToolUse (JSON pe stdin)
v
confirm_hook.py --- clasificator ---> nepericuloasa ---> exit 0, fara iesire (flux normal)
|
| ireversibila
v
~/.claude-discord/approvals/<request_id>.json (status: pending)
| ^
| (bot.py vede cererea prin | submit_decision("allow"|"deny")
| set_on_request si posteaza |
| butoanele in fir) |
v |
polling pe disc, pana la 300s -----------+
|
v
{"hookSpecificOutput": {"permissionDecision": "allow"|"deny", ...}}
Hook-ul si botul sunt procese diferite (hook-ul e pornit de CLI-ul claude), de aceea
canalul dintre ele e un director pe disc si nu memoria botului.
Formatul fisierului de cerere
~/.claude-discord/approvals/<request_id>.json, scris atomic (tmp + os.replace):
{
"request_id": "3f9a1c2b7d4e5f60",
"thread_id": "1234567890",
"session_id": "b1c2...",
"tool_name": "Bash",
"command": "rm -rf /var/lib/vz/dump",
"rule": "rm_recursiv",
"reason": "stergere recursiva (rm -r)",
"cwd": "/workspace/romfastsql",
"created_at": 1756512000.0,
"expires_at": 1756512300.0,
"status": "pending",
"decision": null,
"decided_at": null,
"decided_by": null
}
status:pending->allow/deny. Botul schimba doarstatus,decision,decided_at.thread_idvine din variabila de mediuCLAUDE_DISCORD_THREAD_ID, pe care Lane A o pune in mediul procesuluiclaudeal firului respectiv. Lipsa ei inseamnanullsi cererea ajunge in canalul principal.- Dupa decizie, hook-ul muta fisierul in
approvals/done/<request_id>.json(cufinished_at), capending_requests()sa nu-l mai vada.cleanup_stale()sterge ce e mai vechi de o zi.
API-ul consumat de bot (contract INTERFACES.md)
await approvals.wait_for_decision(request_id, timeout) # "allow" | "deny" (timeout => deny)
approvals.submit_decision(request_id, "allow") # True daca cererea exista
await approvals.pending_requests() # cereri in asteptare
approvals.set_on_request(callback) # callback async la fiecare cerere noua
set_on_request porneste un watcher pe directorul de cereri (poll 0.5s) daca exista o bucla
asyncio activa; set_on_request(None) il opreste. Un callback care arunca nu opreste watcher-ul.
Fail-closed
Orice abatere inseamna deny, cu motiv explicit trimis inapoi in CLI:
- JSON invalid sau payload care nu e obiect;
~/.claude-discordlipseste (hook-ul nu improvizeaza un director nou);- cererea nu poate fi scrisa pe disc;
- fisierul cererii dispare sau devine JSON corupt in timpul asteptarii;
- niciun raspuns in
CLAUDE_DISCORD_APPROVAL_TIMEOUTsecunde (implicit 300); - orice alta exceptie, prinsa de plasa finala din
main().
Toate cazurile de mai sus au test in tests/test_confirm_hook.py.
2. Ce prinde clasificatorul
Analizeaza doar tool-ul Bash. Comanda e tokenizata cu shlex (operatorii ;, &&, ||, |
raman token-uri separate), impartita in segmente, iar fiecare segment e curatat de wrappere
(sudo, env FOO=1, timeout 30, nohup, nice, atribuiri VAR=val) inainte de a fi
clasificat pe numele de baza al executabilului (deci /bin/rm = rm). Intra recursiv in
bash -c "...", sh -c "...", ssh host "...", pct exec ... -- ..., docker exec ... ...
(maxim 5 niveluri).
Reguli: rm -r, rm -f pe cai de sistem, find -delete, shred, dd, mkfs*, wipefs,
fdisk/parted/sgdisk, redirectare in /dev/... (mai putin /dev/null|stdout|stderr|tty),
shutdown/reboot/halt/poweroff/init 0|6, pct|qm destroy|restore, pvesh delete,
pvesm remove|free, pveceph destroy*|purge, zfs destroy|rollback, zpool destroy|labelclear,
lvremove/vgremove/pvremove, systemctl stop|disable|mask|kill pe servicii de infra,
systemctl -H, git push --force, git clean -f, git reset --hard, docker system prune,
docker volume rm, docker rm -f, chmod|chown -R pe cai de sistem, DROP/TRUNCATE pe
obiecte Oracle, si orice ssh/scp/rsync/infra catre un host de productie
(10.0.20.36, .37, .200, .201, .202, pve1, pvemini, pveelite, roacentral).
3. Ce NU prinde (limitele asumate)
Acesta e un strat impotriva accidentelor, nu impotriva unui atacator. Cine controleaza promptul poate ocoli detectia banal. Concret, NU sunt prinse:
- Ofuscarea:
echo cm0gLXJmIC8= | base64 -d | sh,R=rm; $R -rf /tmp/x,python3 -c "import shutil; shutil.rmtree('/x')",perl -e 'unlink...',eval "$CMD". Hook-ul vede text, nu semantica. - Comenzi dintr-un fisier:
./cleanup.sh,make clean,npm run reset, un hook git — continutul scriptului nu e citit. La felsqlplus @drop_all.sql: numele fisierului nu spune ce e inauntru. - Alte tool-uri:
Write,Edit,NotebookEdit, tool-urile MCP nu trec prin clasificator. Un.shdistructiv poate fi scris fara confirmare (rularea lui, insa, se vede caBash). - Variabile si expansiuni:
rm -rf "$DIR"cere confirmare (regularm -r), darrm -f "$FILES"nu, fiindca nu stim ce contine variabila. - API in loc de CLI:
curl -X DELETE https://10.0.20.201:8006/api2/json/nodes/...nu e clasificat ca stergere (doar hostul de productie ar fi prins daca executabilul ar fi ssh/rsync; cucurlnu e). sshcu optiuni cu valoare:ssh -p 2222 host cmdpoate incurca detectarea hostului (valoarea2222e luata drept host), deci comanda remota nu mai e analizata.- Falsuri pozitive acceptate: orice
rm -r, chiar in/tmp, cere confirmare. E zgomot deliberat: un clic e mai ieftin decat un director sters gresit. - Auto-aprobarea: hook-ul si botul ruleaza sub acelasi utilizator, deci un agent care vrea
neaparat isi poate scrie singur
"status": "allow"in fisierul cererii. Consecinta directa a respingerii stratului 3 (user separat / audit independent), asumata explicit.
4. Wrapper infra
infra --list # hosturile permise
infra pvemini pct list # ruleaza comanda pe nodul Proxmox
infra oracle docker ps
INFRA_DRY_RUN=1 infra pvemini uptime # arata comanda ssh, nu o executa
-
Hostul e cautat intr-o lista explicita. Un host absent e refuzat imediat, fara DNS:
exit 3. Fara comanda:exit 2. Fisier de hosturi corupt:exit 4. Altfel, codul de iesire este cel al comenzii remote. -
Lista implicita e in
infra(DEFAULT_HOSTS) si poate fi inlocuita integral cu~/.claude-discord/infra-hosts.json:{ "pvemini": {"addr": "10.0.20.201", "user": "root", "prod": true, "desc": "nod principal"}, "oracle": {"addr": "10.0.20.121", "user": "root"}, "oracle-prod": {"addr": "10.0.20.36", "user": "romfast", "prod": true} }Daca fisierul exista, inlocuieste lista implicita (nu se adauga la ea).
-
Fiecare apel — inclusiv refuzurile — se scrie pe o linie in
~/.claude-discord/logs/infra.log:2026-08-30T11:20:41 host=pvemini target=root@10.0.20.201 rc=0 dur=0.42s cmd=pct list 2026-08-30T11:21:03 host=router.local target=- rc=refuzat dur=0.00s cmd=reboot note=host in afara listeiJurnalul e un ajutor de depanare, nu un audit: ruleaza sub acelasi user si poate fi rescris.
5. Instalare
mkdir -p ~/.claude-discord/{approvals/done,logs}
chmod 700 ~/.claude-discord
# settings pasat botului cu --settings
cp proxmox/lxc171-claude-agent/discord-bridge/security/bot-settings.json.example \
~/.claude-discord/bot-settings.json
# ajusteaza calea absoluta a hook-ului daca repo-ul nu e in /workspace/romfastsql
# wrapper-ul in PATH
ln -s /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge/security/infra ~/bin/infra
Variabile de mediu (puse de Lane A in mediul procesului claude):
| Variabila | Rol | Implicit |
|---|---|---|
CLAUDE_DISCORD_DIR |
muta ~/.claude-discord (teste) |
~/.claude-discord |
CLAUDE_DISCORD_APPROVAL_TIMEOUT |
cat asteapta hook-ul o decizie, in secunde | 300 |
CLAUDE_DISCORD_THREAD_ID |
firul in care se posteaza butoanele | — |
INFRA_DRY_RUN |
infra doar tipareste comanda ssh |
— |
Atentie: timeout din bot-settings.json (330s) trebuie sa ramana mai mare decat
CLAUDE_DISCORD_APPROVAL_TIMEOUT, altfel CLI-ul taie hook-ul inainte sa apuce sa refuze curat.
6. Token Proxmox cu ACL restrans (pasi manuali)
Nu a fost creat nimic pe cluster. Comenzile de mai jos se ruleaza de om, ca root pe
pvemini (10.0.20.201). Tokenul acopera operatiile de citire si control de alimentare pe care
le vrea puntea; VM.Allocate (crearea/distrugerea de guest-uri) este intentionat lasat afara.
# 1. utilizator dedicat pentru punte
pveum user add claude-bridge@pve --comment "punte Discord -> Claude Code (LXC 171)"
# 2. rol cu strictul necesar
# - audit/monitorizare: sa poata raspunde la "ce mai face clusterul"
# - PowerMgmt + Console: start/stop/reboot pe guest si `pct exec`-uri prin API
pveum role add ClaudeBridge -privs "\
Datastore.Audit,\
Sys.Audit,Sys.Console,Sys.Syslog,\
VM.Audit,VM.Monitor,VM.Console,VM.PowerMgmt"
# 3. legarea rolului de utilizator (pe tot arborele; restrange la /vms/<id> daca vrei mai putin)
pveum acl modify / --users claude-bridge@pve --roles ClaudeBridge
# 4. tokenul propriu-zis, cu separare de privilegii activa
pveum user token add claude-bridge@pve discord --privsep 1
# ^ afiseaza SECRETUL O SINGURA DATA. Copiaza-l acum.
# 5. ACL explicit pentru token (necesar cand privsep=1)
pveum acl modify / --tokens 'claude-bridge@pve!discord' --roles ClaudeBridge
# 6. verificare
pveum acl list
pveum user token list claude-bridge@pve
Pe LXC 171, secretul se pune in ~/.claude-discord/env (fisier 0600, deja folosit de Lane A):
PVE_API_URL=https://10.0.20.201:8006/api2/json
PVE_TOKEN_ID=claude-bridge@pve!discord
PVE_TOKEN_SECRET=<secretul afisat la pasul 4>
Test rapid (citeste, nu schimba nimic):
curl -sk -H "Authorization: PVEAPIToken=${PVE_TOKEN_ID}=${PVE_TOKEN_SECRET}" \
"${PVE_API_URL}/nodes" | jq '.data[].node'
Pentru revocare: pveum user token remove claude-bridge@pve discord.
Ce ramane in sarcina omului: pasii 1-6 de mai sus pe pvemini, copierea secretului in
~/.claude-discord/env, chmod 600 pe acel fisier si decizia daca ACL-ul ramane pe / sau se
restrange la un subset de guest-uri. Puntea nu creeaza si nu roteste tokenul singura.
Tokenul nu inlocuieste cheile SSH existente din ~/.ssh — retragerea lor a fost respinsa
deliberat, fiindca accesul SSH la infrastructura e functionalitate ceruta. Tokenul e o cale
alternativa, cu drepturi mai mici, pentru operatiile care se pot face prin API.
7. Teste
cd proxmox/lxc171-claude-agent/discord-bridge
python3 -m pytest tests/test_confirm_hook.py tests/test_infra.py -q
Fara retea, fara Discord, fara cluster. tests/test_infra.py ruleaza totul cu INFRA_DRY_RUN=1,
iar tests/test_confirm_hook.py include si un test in care hook-ul e pornit ca proces separat si
aprobat din exterior — exact granita reala dintre hook si bot.