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
6.2 KiB
INTERFACES — contract intre lane-uri (proprietate: orchestrator, NU modifica)
Cele trei lane-uri lucreaza in acelasi director. Acest fisier fixeaza cine ce fisier scrie si ce semnaturi trec granita, ca merge-ul sa fie mecanic.
Proprietate pe fisiere (STRICTA — nu scrie in fisierele altui lane)
| Lane | Fisiere pe care le creeaza/editeaza |
|---|---|
| A (nucleu + adaptor) | session_store.py, stream.py, runner.py, render.py, limits.py, bot.py, config.py, tests/**, requirements.txt, requirements-dev.txt |
| B (securitate) | security/confirm_hook.py, security/infra, security/approvals.py, security/bot-settings.json.example, security/README.md, tests/test_confirm_hook.py, tests/test_infra.py |
| C (ops) | alerts.py, cleanup.py, dashboard/**, ops/claude-discord.service, ops/install.sh, ops/logrotate.conf, tests/test_alerts.py, tests/test_cleanup.py, tests/test_dashboard.py, README.md, si liniile de index din ../README.md + /workspace/romfastsql/CLAUDE.md |
Fisiere partajate ca citire: acest INTERFACES.md. Nimeni nu-l editeaza.
Layout runtime (in afara repo)
~/.claude-discord/
env # 0600: DISCORD_TOKEN, allowlist, ALERT_RECIPIENT, COST_CAP_USD_DAY
bot-settings.json # settings pasat cu --settings (hook PreToolUse) — Lane B
state.json # stare sesiuni — Lane A
venv/
logs/bot.log
Modelul de date state.json (Lane A e autoritatea)
{
"version": 1,
"threads": {
"<discord_thread_or_channel_id>": {
"sid": "uuid sesiune claude sau null",
"cwd": "/workspace/<proiect>",
"model": "sonnet",
"pid": 12345,
"pid_start_time": 987654.21,
"inflight": {"turn_id": "...", "started_at": 1756512000.0, "user_id": "...", "message_id": "..."},
"cost_usd_total": 0.0,
"last_active": 1756512000.0
}
},
"cost": {"day": "2026-08-30", "usd": 0.0}
}
pid_start_time = /proc/<pid>/stat campul 22 (starttime), convertit in SECUNDE de la
boot (ticks / os.sysconf('SC_CLK_TCK')), pentru detectarea PID reuse. Unitatea conteaza:
un consumator care compara direct ticks-urile din /proc cu valoarea din state.json nu se
va potrivi NICIODATA, iar daca acea comparatie protejeaza ceva (cleanup.py), esecul e tacut
si periculos — tot ce trebuia protejat devine eligibil pentru omorare.
Granita A <-> B (aprobari)
Lane B expune security/approvals.py. Lane A il importa si nu-i cunoaste interiorul.
# security/approvals.py — implementat de Lane B, consumat de Lane A
async def wait_for_decision(request_id: str, timeout: float) -> str: ...
# returneaza "allow" | "deny"; la timeout returneaza "deny" (fail-closed)
def submit_decision(request_id: str, decision: str) -> bool: ...
# apelat de bot.py cand utilizatorul apasa butonul; True daca cererea exista
async def pending_requests() -> list[dict]: ...
# [{"request_id", "thread_id", "tool_name", "command", "created_at"}]
def set_on_request(callback) -> None: ...
# Lane A inregistreaza aici un async callback(request: dict) apelat cand
# hook-ul cere o confirmare; bot.py posteaza atunci butoanele in fir.
Canalul hook -> bot e un director de cereri pe disc (~/.claude-discord/approvals/), fiindca
hook-ul PreToolUse ruleaza intr-un proces separat, nu in botul Python. Lane B alege formatul;
Lane A vede doar functiile de mai sus.
Fail-closed e obligatoriu: orice eroare, timeout sau fisier corupt => deny.
Granita A <-> C (alerte)
Lane C expune alerts.py. Lane A il apeleaza in caile de esec.
# alerts.py — implementat de Lane C, consumat de Lane A
def alert(level: str, subject: str, body: str, dedup_key: str | None = None) -> None: ...
# level: "INFO" | "WARN" | "CRITICAL"
# trimite email prin `mail -s "[LEVEL] subject" "$ALERT_RECIPIENT"` (conventia repo,
# vezi proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh)
# NU arunca niciodata exceptii — o alerta esuata nu are voie sa doboare botul
# dedup_key: aceeasi cheie nu retrimite in fereastra de 1h
Conditiile pe care Lane A le semnaleaza (T12): proces mort neasteptat, crash loop, plafon de cost atins, state.json corupt, orfani detectati la sweep.
Granita A <-> C (cleanup)
# cleanup.py — implementat de Lane C, consumat de bot.py pentru comanda !cleanup
def find_orphans(state: dict) -> list[dict]: ...
# procese `claude` din cgroup-ul serviciului care nu apar in state.json,
# plus copii lasati in urma (servere pornite in tururi anterioare)
# -> [{"pid", "cmdline", "age_s", "rss_mb"}]
def kill_orphans(orphans: list[dict], dry_run: bool = True) -> list[dict]: ...
Granita comuna: config
Lane A creeaza config.py, care citeste ~/.claude-discord/env. B si C il importa
pentru cai si setari; nu-si citesc singure env-ul.
# config.py — implementat de Lane A
STATE_DIR: pathlib.Path # ~/.claude-discord
APPROVALS_DIR: pathlib.Path # ~/.claude-discord/approvals
STATE_FILE: pathlib.Path
LOG_DIR: pathlib.Path
def get(key: str, default=None) -> str | None: ... # citeste din env-ul incarcat
Daca config.py nu exista inca la momentul in care B sau C au nevoie de el (lane-uri paralele),
scrie codul care il importa oricum — se rezolva la merge — si NU crea o varianta proprie.
Reguli de test
pytestfara marker: zero retea, zero Discord, zero API. Suita sub 3s.- Testele care ating CLI-ul real:
@pytest.mark.e2e, excluse implicit prinpytest.ini(Lane A scriepytest.inicuaddopts = -m "not e2e"). - Fiecare lane isi scrie doar propriile fisiere de test, dupa tabelul de proprietate.
Decizii deja luate (nu le redeschide)
--permission-mode bypassPermissionse intentionat; deny rules sunt strat cosmetic, nu bariera.- Accesul larg la /workspace si la infrastructura e FUNCTIONALITATE ceruta, nu bug.
- Fara user separat
cdbot, fara audit append-only, fara dashboard web, fara Agent SDK. - Fara reluare automata a turului pierdut (risc de dubla executie).
- Model default
sonnet;!model opusper fir. - Confirmarile se pot memora pe fir (buton "Allow (tot firul)"), pe tiparul
(rule, reason), nu pe comanda si nu pesession_id(acela se schimba la--resume).