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

@@ -299,6 +299,33 @@ def test_decizia_ajunge_la_lane_b(bridge, allowed, monkeypatch):
assert "Permis" in out
def test_allow_pe_fir_spune_ca_nu_mai_intreaba(bridge, allowed, monkeypatch):
monkeypatch.setattr(bot.approvals, "submit_decision", lambda rid, dec: True)
out = bridge.decide(USER, "req-1", "allow_session")
assert "nu se mai" in out and "/permisiuni" in out
def test_permisiuni_listeaza_si_revoca(bridge, allowed):
assert "Nicio aprobare" in bridge.permissions_text("200")
bot.approvals.add_grant("200", "host_productie", "comanda catre hostul pvemini")
assert "host_productie" in bridge.permissions_text("200")
assert "Am revocat 1" in bridge.permissions_text("200", revoke=True)
assert "Nicio aprobare" in bridge.permissions_text("200")
def test_permisiuni_fara_lane_b_nu_arunca(bridge, monkeypatch):
monkeypatch.setattr(bot, "approvals", None)
assert "nu e disponibil" in bridge.permissions_text("200")
async def test_sesiune_noua_revoca_aprobarile_firului(bridge, allowed):
bot.approvals.add_grant("200", "rm_recursiv", "stergere recursiva (rm -r)")
ch = FakeChannel()
await bridge.handle_slash(FakeInteraction(channel=ch), "new")
assert "Sesiune noua" in ch.all_text and "revocat si 1" in ch.all_text
assert bot.approvals.list_grants("200") == []
def test_cerere_inexistenta_nu_arunca(bridge, allowed):
assert "nu mai exista" in bridge.decide(USER, "req-inexistent", "deny")