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

6.0 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

  • 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.