# 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`, `ops/claude-discord.service`, `ops/install.sh`, `ops/logrotate.conf`, `tests/test_alerts.py`, `tests/test_cleanup.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) ```json { "version": 1, "threads": { "": { "sid": "uuid sesiune claude sau null", "cwd": "/workspace/", "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//stat` field 22, pentru detectarea PID reuse. ## Granita A <-> B (aprobari) Lane B expune `security/approvals.py`. Lane A il importa si nu-i cunoaste interiorul. ```python # 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. ```python # 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) ```python # 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. ```python # 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 - `pytest` fara marker: zero retea, zero Discord, zero API. Suita sub 3s. - Testele care ating CLI-ul real: `@pytest.mark.e2e`, excluse implicit prin `pytest.ini` (Lane A scrie `pytest.ini` cu `addopts = -m "not e2e"`). - Fiecare lane isi scrie doar propriile fisiere de test, dupa tabelul de proprietate. ## Decizii deja luate (nu le redeschide) - `--permission-mode bypassPermissions` e 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 opus` per fir.