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:
Claude Agent
2026-08-31 07:36:34 +00:00
parent feb31eaabe
commit 1cc1a572d9
13 changed files with 526 additions and 14 deletions

View File

@@ -406,6 +406,17 @@ def run(payload_text: str) -> int:
except Exception as exc:
return _deny(f"hook de confirmare: modulul de aprobari lipseste ({exc})")
# Firul Discord e "sesiunea": aprobarea data o data acolo tine pentru tot
# firul. Orice eroare aici inseamna doar ca intrebam din nou (fail-closed).
scope_id = os.environ.get("CLAUDE_DISCORD_THREAD_ID")
try:
already = approvals.has_grant(scope_id, rule, reason)
except Exception:
already = False
if already:
_log(f"ALLOW (aprobat pe fir {scope_id}) rule={rule} cmd={command[:160]}")
return _allow(f"aprobat pentru firul curent ({reason})")
timeout = _timeout_s()
try:
req = approvals.create_request(
@@ -434,7 +445,25 @@ def run(payload_text: str) -> int:
_log(f"DENY {rid} exceptie la asteptare: {exc}")
if decision == "allow":
scope = "once"
try:
scope = approvals.read_scope(rid)
except Exception:
scope = "once"
approvals.finish_request(rid, "allow")
if scope == "thread":
saved = False
try:
saved = approvals.add_grant(
scope_id, rule, reason, session_id=payload.get("session_id")
)
except Exception:
saved = False
_log(f"ALLOW {rid} rule={rule} scope=fir salvat={saved}")
if saved:
return _allow(
f"aprobat in Discord pentru tot firul (cerere {rid}, {reason})"
)
_log(f"ALLOW {rid} rule={rule}")
return _allow(f"aprobat in Discord (cerere {rid}, {reason})")