Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/INTERFACES.md
Claude Agent 7abefa2b46 feat(discord-bridge): dashboard de control si restart, dupa modelul agentului echo
Panou web pe 127.0.0.1:18790, unit systemd separat de al puntii. Server stdlib
(fara dependinte noi), tokenii de design si tiparul de endpoint-uri preluate din
/home/moltbot/echo-core/dashboard (handlers/eco.py) de pe LXC 110.

Arata: starea unitatii (uptime, PID, memoria cgroup, restarturi), firele din
state.json cu tur in zbor si cost, costul zilei fata de plafon, confirmarile
PreToolUse in asteptare (aprobabile direct din pagina), bot.log / infra.log si
opt verificari de diagnostic.

Face: start / stop / restart pe punte, cautarea si curatarea orfanilor prin
cleanup.py, repornirea propriului serviciu.

Garantii, cu teste:
- unitatea controlata e fixa in cod; un {"unit": "ssh.service"} in cerere nu
  schimba nimic, altfel panoul ar fi systemctl remote fara parola;
- stop/restart intorc 409 cu lista firelor active si cer force explicit, fiindca
  KillMode=control-group taie tururile in desfasurare;
- state.json se citeste fara lock: panoul nu are voie sa blocheze botul;
- diagnosticul pica daca reapare Bash(ssh:*) in deny (regresia de azi).

Uptime-ul se calculeaza din time.monotonic(), nu din /proc/uptime: in LXC acela
e virtualizat de lxcfs si da diferenta negativa fata de monotonic-ul systemd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
2026-08-30 13:18:38 +00:00

139 lines
6.0 KiB
Markdown

# 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)
```json
{
"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.
```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.