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:
@@ -206,10 +206,14 @@ class Command:
|
||||
return " ".join(self.args)
|
||||
|
||||
|
||||
COMMANDS = ("new", "cd", "model", "status", "stop", "cleanup", "help")
|
||||
COMMANDS = ("new", "cd", "model", "status", "stop", "cleanup", "permisiuni", "help")
|
||||
|
||||
# Optiunile slash traduse in argumentele pe care cmd_* le stiu deja.
|
||||
SLASH_FLAGS = {"new": ("fork", "--fork"), "cleanup": ("force", "--force")}
|
||||
SLASH_FLAGS = {
|
||||
"new": ("fork", "--fork"),
|
||||
"cleanup": ("force", "--force"),
|
||||
"permisiuni": ("revoca", "revoca"),
|
||||
}
|
||||
|
||||
|
||||
def slash_args(name: str, opts: dict) -> list[str]:
|
||||
@@ -403,7 +407,7 @@ class Bridge:
|
||||
await channel.send(text)
|
||||
|
||||
def approval_view(self, request_id: str):
|
||||
"""View cu Allow/Deny. Fara discord.py (sau fara Lane B) returneaza None."""
|
||||
"""View cu Allow / Allow-pe-fir / Deny. Fara discord.py returneaza None."""
|
||||
if discord is None or approvals is None:
|
||||
return None
|
||||
view = discord.ui.View(timeout=None)
|
||||
@@ -420,6 +424,7 @@ class Bridge:
|
||||
return button
|
||||
|
||||
view.add_item(_mk("Allow", "allow", discord.ButtonStyle.success))
|
||||
view.add_item(_mk("Allow (tot firul)", "allow_session", discord.ButtonStyle.primary))
|
||||
view.add_item(_mk("Deny", "deny", discord.ButtonStyle.danger))
|
||||
return view
|
||||
|
||||
@@ -435,6 +440,12 @@ class Bridge:
|
||||
ok = bool(approvals.submit_decision(request_id, decision))
|
||||
if not ok:
|
||||
return "⚠️ Cererea nu mai exista (expirata sau deja decisa)."
|
||||
if decision == "allow_session":
|
||||
return (
|
||||
f"✅ Permis de <@{user_id}> — **operatiuni de acelasi fel nu se mai "
|
||||
"confirma in acest fir** (`/permisiuni` le listeaza, "
|
||||
"`/permisiuni revoca` le sterge)."
|
||||
)
|
||||
return f"{'✅ Permis' if decision == 'allow' else '🚫 Refuzat'} de <@{user_id}>."
|
||||
|
||||
# ------------------------------------------------------------- intrare
|
||||
@@ -537,6 +548,7 @@ class Bridge:
|
||||
"`/model model:<sonnet|opus>` schimba modelul firului\n"
|
||||
"`/status` starea firului · `/stop` opreste turul curent\n"
|
||||
"`/cleanup [force:True]` procese lasate in urma\n"
|
||||
"`/permisiuni [revoca:True]` ce s-a aprobat pentru tot firul\n"
|
||||
"Orice alt mesaj din canal pleaca la Claude ca prompt.",
|
||||
)
|
||||
|
||||
@@ -548,10 +560,16 @@ class Bridge:
|
||||
if not fork:
|
||||
fields["sid"] = None
|
||||
self.store.update_thread(tid, **fields)
|
||||
# Sesiune noua = permisiuni noi: aprobarile "pe tot firul" nu se mostenesc.
|
||||
revoked = 0
|
||||
if approvals is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
revoked = int(approvals.clear_grants(tid))
|
||||
await self.say(
|
||||
message.channel,
|
||||
"🧵 Sesiune noua pornita din contextul celei curente (`--fork`)."
|
||||
if fork else "🧵 Sesiune noua, curata.",
|
||||
("🧵 Sesiune noua pornita din contextul celei curente (`--fork`)."
|
||||
if fork else "🧵 Sesiune noua, curata.")
|
||||
+ (f" Am revocat si {revoked} aprobare(-ri) pe fir." if revoked else ""),
|
||||
)
|
||||
|
||||
async def cmd_cd(self, message, tid: str, cmd: Command) -> None:
|
||||
@@ -620,6 +638,38 @@ class Bridge:
|
||||
lines.append("stderr (ultimele linii):\n```\n" + body + "\n```")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def cmd_permisiuni(self, message, tid: str, cmd: Command) -> None:
|
||||
await self.say(message.channel, self.permissions_text(tid, "revoca" in cmd.args))
|
||||
|
||||
def permissions_text(self, thread_id: str, revoke: bool = False) -> str:
|
||||
"""Ce s-a aprobat pe firul asta cu butonul "Allow (tot firul)"."""
|
||||
if approvals is None:
|
||||
return "⚠️ Modulul de aprobari nu e disponibil (Lane B)."
|
||||
if revoke:
|
||||
n = 0
|
||||
with contextlib.suppress(Exception):
|
||||
n = int(approvals.clear_grants(thread_id))
|
||||
return (
|
||||
f"🔒 Am revocat {n} aprobare(-ri) pe fir. Urmatoarele operatiuni "
|
||||
"periculoase cer din nou confirmare."
|
||||
if n
|
||||
else "Nu era nimic de revocat pe firul asta."
|
||||
)
|
||||
grants = []
|
||||
with contextlib.suppress(Exception):
|
||||
grants = approvals.list_grants(thread_id)
|
||||
if not grants:
|
||||
return (
|
||||
"Nicio aprobare memorata pe firul asta: fiecare operatiune "
|
||||
"periculoasa se confirma individual."
|
||||
)
|
||||
lines = ["**Aprobate pentru firul curent**", "```"]
|
||||
for g in grants:
|
||||
age = max(0, int((time.time() - float(g.get("granted_at") or 0)) // 60))
|
||||
lines.append(f"{g.get('rule', '?'):<18} {g.get('reason', '')} (acum {age} min)")
|
||||
lines += ["```", "`/permisiuni revoca` le sterge pe toate."]
|
||||
return "\n".join(lines)
|
||||
|
||||
async def cmd_stop(self, message, tid: str, cmd: Command) -> None:
|
||||
proc = self.runner.procs.get(tid)
|
||||
if proc is None or not proc.alive:
|
||||
|
||||
Reference in New Issue
Block a user