feat(discord-bridge): aprobari valabile pe tot firul (buton "Allow (tot firul)")
Confirmarea per comanda devenea obositoare intr-o sesiune care lucreaza pe acelasi host: `ssh pvemini ...` de zece ori la rand insemna zece butoane. Butonul de confirmare are acum trei variante: Allow / Allow (tot firul) / Deny. "Allow (tot firul)" memoreaza tiparul `(rule, reason)` produs de clasificator, nu comanda: dupa o aprobare pe `ssh pvemini uptime`, orice comanda catre ACEL host trece singura, dar `ssh 10.0.20.36` sau un `rm -rf` cer din nou confirmare. Aprobarile stau in ~/.claude-discord/approvals/grants/<fir>.json. Domeniul e firul Discord, nu `session_id`: acela se schimba la `--resume`, iar aprobarile ar disparea exact cand omul se astepta sa tina. Expirare: `/new` le sterge (sesiune noua = permisiuni noi), `/permisiuni revoca:True` la cerere, TTL implicit 12h (CLAUDE_DISCORD_GRANT_TTL), iar CLAUDE_DISCORD_SESSION_GRANTS=off dezactiveaza complet mecanismul. Fail-closed peste tot, ca restul hook-ului: fara CLAUDE_DISCORD_THREAD_ID (hook rulat in afara puntii), cu fisierul de aprobari corupt, cu un thread_id care nu arata a id (`../`, punct la inceput, peste 128 de caractere) sau la orice exceptie, has_grant() raspunde False si se cere confirmare in Discord. Adaugat si `/permisiuni [revoca:True]` (listare/revocare) plus butonul echivalent in dashboard (`decision: "allow_session"`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
@@ -71,20 +71,87 @@ canalul dintre ele e un director pe disc si nu memoria botului.
|
||||
}
|
||||
```
|
||||
|
||||
- `status`: `pending` -> `allow` / `deny`. Botul schimba doar `status`, `decision`, `decided_at`.
|
||||
- `status`: `pending` -> `allow` / `deny`. Botul schimba doar `status`, `decision`, `decided_at`
|
||||
si `scope`.
|
||||
- `thread_id` vine din variabila de mediu `CLAUDE_DISCORD_THREAD_ID`, pe care Lane A o pune in
|
||||
mediul procesului `claude` al firului respectiv. Lipsa ei inseamna `null` si cererea ajunge
|
||||
in canalul principal.
|
||||
- Dupa decizie, hook-ul muta fisierul in `approvals/done/<request_id>.json` (cu `finished_at`),
|
||||
ca `pending_requests()` sa nu-l mai vada. `cleanup_stale()` sterge ce e mai vechi de o zi.
|
||||
|
||||
## 1b. Aprobari valabile pe tot firul ("nu ma mai intreba")
|
||||
|
||||
Confirmarea per comanda devine obositoare intr-o sesiune care lucreaza pe acelasi host:
|
||||
`ssh pvemini ...` de zece ori la rand inseamna zece butoane. De aceea butonul de confirmare
|
||||
are trei variante:
|
||||
|
||||
| Buton | Ce face |
|
||||
|---|---|
|
||||
| **Allow** | permite comanda asta si atat |
|
||||
| **Allow (tot firul)** | permite comanda si **memoreaza tiparul** pentru firul curent |
|
||||
| **Deny** | refuza |
|
||||
|
||||
Ce se memoreaza nu e comanda, ci perechea `(rule, reason)` produsa de clasificator:
|
||||
|
||||
```
|
||||
host_productie|comanda catre hostul de productie 10.0.20.201
|
||||
serviciu_infra|systemctl stop pe serviciul de infra oracle-xe
|
||||
rm_recursiv|stergere recursiva (rm -r)
|
||||
```
|
||||
|
||||
Asa aprobarea e utila fara sa fie oarba: dupa un „Allow (tot firul)" pe `ssh pvemini uptime`,
|
||||
orice comanda catre **acel** host trece singura, dar `ssh 10.0.20.36` sau un `rm -rf` cer din
|
||||
nou confirmare. Aprobarile stau in
|
||||
|
||||
```
|
||||
~/.claude-discord/approvals/grants/<thread_id>.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"thread_id": "1234567890",
|
||||
"created_at": 1756512000.0,
|
||||
"updated_at": 1756512130.0,
|
||||
"grants": {
|
||||
"host_productie|comanda catre hostul de productie 10.0.20.201": {
|
||||
"rule": "host_productie",
|
||||
"reason": "comanda catre hostul de productie 10.0.20.201",
|
||||
"granted_at": 1756512130.0,
|
||||
"granted_by": null,
|
||||
"session_id": "b1c2..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Domeniul e firul Discord, nu id-ul de sesiune Claude.** Un `--resume` poate schimba
|
||||
`session_id`, iar aprobarile ar disparea exact cand omul se astepta sa tina. Firul e ce vede
|
||||
utilizatorul si e stabil.
|
||||
|
||||
Cand expira:
|
||||
|
||||
- **`/new`** (sesiune noua in fir) le sterge — sesiune noua, permisiuni noi;
|
||||
- **`/permisiuni revoca:True`** le sterge la cerere; `/permisiuni` le listeaza;
|
||||
- automat dupa `CLAUDE_DISCORD_GRANT_TTL` secunde (implicit 12h — o zi de lucru, nu vesnicia);
|
||||
- `CLAUDE_DISCORD_SESSION_GRANTS=off` dezactiveaza complet mecanismul (se revine la
|
||||
confirmare per comanda).
|
||||
|
||||
Si aici regula e **fail-closed**: fara `CLAUDE_DISCORD_THREAD_ID` (hook rulat in afara puntii),
|
||||
cu fisierul de aprobari corupt, cu un `thread_id` care nu arata a id (`../`, punct la inceput,
|
||||
peste 128 de caractere) sau la orice exceptie, `has_grant()` raspunde `False` si se cere
|
||||
confirmare in Discord ca pana acum.
|
||||
|
||||
### API-ul consumat de bot (contract INTERFACES.md)
|
||||
|
||||
```python
|
||||
await approvals.wait_for_decision(request_id, timeout) # "allow" | "deny" (timeout => deny)
|
||||
approvals.submit_decision(request_id, "allow") # True daca cererea exista
|
||||
approvals.submit_decision(request_id, "allow_session") # allow + scope="thread"
|
||||
await approvals.pending_requests() # cereri in asteptare
|
||||
approvals.set_on_request(callback) # callback async la fiecare cerere noua
|
||||
|
||||
approvals.list_grants(thread_id) # aprobarile valabile ale firului
|
||||
approvals.clear_grants(thread_id) # cate a revocat
|
||||
```
|
||||
|
||||
`set_on_request` porneste un watcher pe directorul de cereri (poll 0.5s) daca exista o bucla
|
||||
@@ -98,6 +165,7 @@ Orice abatere inseamna **deny**, cu motiv explicit trimis inapoi in CLI:
|
||||
- `~/.claude-discord` lipseste (hook-ul nu improvizeaza un director nou);
|
||||
- cererea nu poate fi scrisa pe disc;
|
||||
- fisierul cererii dispare sau devine JSON corupt in timpul asteptarii;
|
||||
- fisierul de aprobari pe fir lipseste, e corupt, expirat sau fara `thread_id` valid;
|
||||
- niciun raspuns in `CLAUDE_DISCORD_APPROVAL_TIMEOUT` secunde (implicit 300);
|
||||
- orice alta exceptie, prinsa de plasa finala din `main()`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user