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
122 lines
5.1 KiB
Python
122 lines
5.1 KiB
Python
"""Comenzile slash ale puntii: declararea si inregistrarea lor in Discord.
|
|
|
|
Tot ce depinde de `discord.app_commands` sta aici. Logica ramane in `bot.Bridge`
|
|
(`cmd_new`, `cmd_cd`, ...), deci comenzile slash si allowlist-ul nu au a doua
|
|
implementare care sa divergheze.
|
|
|
|
Modulul se importa si FARA discord.py instalat (ca `bot.py`): atunci `build_tree`
|
|
returneaza None si `sync_guilds` nu face nimic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
log = logging.getLogger("discord-bridge.slash")
|
|
|
|
try: # pragma: no cover - depinde de mediu
|
|
import discord # type: ignore
|
|
from discord import app_commands # type: ignore
|
|
except ImportError: # pragma: no cover
|
|
discord = None # type: ignore
|
|
app_commands = None # type: ignore
|
|
|
|
# Permisiunile din README (309237763136) + scope-ul de comenzi. Fara
|
|
# `applications.commands` in invitatie, sync-ul da 403 Missing Access.
|
|
INVITE_TEMPLATE = (
|
|
"https://discord.com/oauth2/authorize?client_id={app_id}"
|
|
"&scope=bot%20applications.commands&permissions=309237763136"
|
|
)
|
|
|
|
|
|
def invite_url(app_id) -> str:
|
|
return INVITE_TEMPLATE.format(app_id=app_id or "APPLICATION_ID")
|
|
|
|
|
|
def build_tree(client, bridge):
|
|
"""Construieste arborele de comenzi. Fara discord.py: None."""
|
|
if app_commands is None: # pragma: no cover - mediu fara discord.py
|
|
log.warning("discord.py lipseste: nu declar comenzi slash")
|
|
return None
|
|
|
|
tree = app_commands.CommandTree(client)
|
|
|
|
@tree.command(name="new", description="Sesiune Claude noua in firul curent")
|
|
@app_commands.describe(fork="Porneste noua sesiune din contextul celei curente")
|
|
async def _new(interaction, fork: bool = False):
|
|
await bridge.handle_slash(interaction, "new", fork=fork)
|
|
|
|
@tree.command(name="cd", description="Schimba directorul de lucru al firului")
|
|
@app_commands.describe(cale="Cale absoluta, oriunde in /workspace")
|
|
async def _cd(interaction, cale: str):
|
|
await bridge.handle_slash(interaction, "cd", cale=cale)
|
|
|
|
@tree.command(name="model", description="Schimba modelul pentru firul curent")
|
|
@app_commands.describe(model="sonnet (implicit) sau opus")
|
|
@app_commands.choices(model=[
|
|
app_commands.Choice(name="sonnet", value="sonnet"),
|
|
app_commands.Choice(name="opus", value="opus"),
|
|
])
|
|
async def _model(interaction, model: app_commands.Choice[str]):
|
|
await bridge.handle_slash(interaction, "model", model=model.value)
|
|
|
|
@tree.command(name="status", description="Sesiune, director, model, cost, proces")
|
|
async def _status(interaction):
|
|
await bridge.handle_slash(interaction, "status")
|
|
|
|
@tree.command(name="stop", description="Opreste turul in desfasurare din firul curent")
|
|
async def _stop(interaction):
|
|
await bridge.handle_slash(interaction, "stop")
|
|
|
|
@tree.command(name="cleanup", description="Procese lasate in urma (implicit: rulare seaca)")
|
|
@app_commands.describe(force="Opreste efectiv procesele gasite (implicit: doar lista)")
|
|
async def _cleanup(interaction, force: bool = False):
|
|
await bridge.handle_slash(interaction, "cleanup", force=force)
|
|
|
|
@tree.command(name="permisiuni", description="Ce s-a aprobat pentru tot firul")
|
|
@app_commands.describe(revoca="Sterge aprobarile firului; totul cere iar confirmare")
|
|
async def _permisiuni(interaction, revoca: bool = False):
|
|
await bridge.handle_slash(interaction, "permisiuni", revoca=revoca)
|
|
|
|
@tree.command(name="help", description="Lista comenzilor puntii")
|
|
async def _help(interaction):
|
|
await bridge.handle_slash(interaction, "help")
|
|
|
|
return tree
|
|
|
|
|
|
async def sync_guilds(tree, guild_ids) -> dict[str, str]:
|
|
"""Inregistreaza comenzile pe fiecare guild din allowlist.
|
|
|
|
Sync-ul pe guild e instantaneu; cel global se propaga in ~1h, deci nu-l folosim.
|
|
NU arunca niciodata: daca invitatia botului nu are scope-ul `applications.commands`,
|
|
Discord raspunde 403 Missing Access — logam linkul de reinvitare si mergem mai
|
|
departe. Botul ramane functional pentru mesajele obisnuite.
|
|
"""
|
|
results: dict[str, str] = {}
|
|
if tree is None or discord is None:
|
|
log.warning("fara arbore de comenzi: sar peste inregistrarea comenzilor slash")
|
|
return results
|
|
gids = [str(g).strip() for g in (guild_ids or []) if str(g).strip()]
|
|
if not gids:
|
|
log.warning("DISCORD_GUILD_IDS gol: nu inregistrez comenzi slash nicaieri")
|
|
return results
|
|
|
|
app_id = getattr(getattr(tree, "client", None), "application_id", None)
|
|
for gid in gids:
|
|
try:
|
|
guild = discord.Object(id=int(gid))
|
|
tree.copy_global_to(guild=guild)
|
|
cmds = await tree.sync(guild=guild)
|
|
results[gid] = f"ok ({len(cmds)} comenzi)"
|
|
log.info("comenzi slash inregistrate pe guild %s: %d", gid, len(cmds))
|
|
except Exception as exc:
|
|
results[gid] = f"esuat: {exc}"
|
|
log.error("sync de comenzi slash esuat pe guild %s: %s", gid, exc)
|
|
log.error(
|
|
"daca e 403 Missing Access, botul a fost invitat fara scope-ul "
|
|
"`applications.commands`. Reinvita-l cu: %s",
|
|
invite_url(app_id),
|
|
)
|
|
return results
|