Implementeaza planul claude-master-plan-discord-bridge-20260830 (15 taskuri, 3 lane-uri paralele) — un bot subtire discord.py peste CLI-ul `claude`, cu proces persistent per fir alimentat pe stdin cu --input-format stream-json. Nucleu: runner (proces persistent + reaper 20min + respawn --resume), stream (parser tolerant), session_store (scriere atomica, lock per fir, detectare PID reuse, recovery), limits (max 4 procese, timeout tur, rate per user, plafon cost pe zi), render (un loop de editare per canal, interval adaptiv). Adaptor: allowlist guild/canal/user fail-closed cu respingerea webhook-urilor, comenzi !new/!cd/!model/!status/!stop/!cleanup, cost si model in subsolul fiecarui raspuns. Mesajul sosit in timpul unui tur devine steering, nu tur nou. Securitate: hook PreToolUse fail-closed care cere confirmare in Discord pentru operatiuni ireversibile, wrapper `infra` cu lista explicita de hosturi. Deny rules raman strat cosmetic, nu bariera (verificat: /usr/bin/ssh trece pe langa). Ops: alerte email pe conventia repo-ului, !cleanup pentru orfani, unit systemd user cu KillMode=control-group si limite de memorie, install.sh idempotent. Verificat: 275 teste fara retea/Discord/API (10.8s), identic cu si fara discord.py instalat; e2e pe CLI real confirma steering-ul mid-tur (mesaj la 6s intr-un tool call de 25s schimba raspunsul final). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
193 lines
6.9 KiB
Python
Executable File
193 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""infra -- singura poarta prin care puntea Discord atinge infrastructura.
|
|
|
|
infra <host> <comanda...> ruleaza comanda pe hostul din lista
|
|
infra --list arata hosturile permise
|
|
infra --help
|
|
|
|
Hosturile sunt o lista EXPLICITA. Un host care nu e in lista este refuzat, fara
|
|
incercare de rezolvare DNS. Fiecare apel este jurnalizat in
|
|
~/.claude-discord/logs/infra.log cu data, host, comanda completa si rezultat.
|
|
|
|
Lista implicita poate fi inlocuita cu ~/.claude-discord/infra-hosts.json:
|
|
|
|
{"pvemini": {"addr": "10.0.20.201", "user": "root", "prod": true,
|
|
"desc": "nod Proxmox principal"}}
|
|
|
|
Coduri de iesire proprii wrapper-ului (comenzile remote isi pastreaza codul lor):
|
|
2 utilizare gresita (lipseste hostul sau comanda)
|
|
3 host in afara listei
|
|
4 eroare de configurare (fisier de hosturi corupt)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
EXIT_USAGE = 2
|
|
EXIT_UNKNOWN_HOST = 3
|
|
EXIT_CONFIG = 4
|
|
|
|
# Lista implicita, din tabelul de retea al repo-ului (CLAUDE.md, proxmox/README.md).
|
|
DEFAULT_HOSTS: dict[str, dict] = {
|
|
# noduri Proxmox
|
|
"pve1": {"addr": "10.0.20.200", "user": "root", "prod": True, "desc": "nod Proxmox pve1"},
|
|
"pvemini": {"addr": "10.0.20.201", "user": "root", "prod": True, "desc": "nod Proxmox principal"},
|
|
"pveelite": {"addr": "10.0.20.202", "user": "root", "prod": True, "desc": "nod Proxmox pveelite"},
|
|
# servicii interne
|
|
"oracle": {"addr": "10.0.20.121", "user": "root", "prod": False, "desc": "LXC 108 Oracle XE 21c/18c"},
|
|
"flowise": {"addr": "10.0.20.161", "user": "root", "prod": False, "desc": "LXC 104 Flowise"},
|
|
"gitea": {"addr": "10.0.20.165", "user": "root", "prod": False, "desc": "LXC Gitea"},
|
|
"docker": {"addr": "10.0.20.113", "user": "root", "prod": False, "desc": "LXC 102 Docker + Portainer"},
|
|
"moltbot": {"addr": "10.0.20.173", "user": "root", "prod": False, "desc": "LXC 110 MoltBot"},
|
|
"dokploy": {"addr": "10.0.20.167", "user": "root", "prod": False, "desc": "LXC 103 Dokploy"},
|
|
# productie / clienti
|
|
"oracle-prod": {"addr": "10.0.20.36", "user": "romfast", "prod": True, "desc": "server Oracle de PRODUCTIE"},
|
|
"oracle-dr": {"addr": "10.0.20.37", "user": "romfast", "prod": True, "desc": "server Oracle DR"},
|
|
"roacentral": {"addr": "10.0.20.122", "user": "romfast", "prod": True, "desc": "VM 201 Windows, IIS reverse proxy"},
|
|
"oracle-test": {"addr": "10.0.20.130", "user": "romfast", "prod": False, "desc": "VM 302 mediu de test"},
|
|
}
|
|
|
|
SSH_OPTS = [
|
|
"-o", "BatchMode=yes",
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", "ConnectTimeout=10",
|
|
]
|
|
|
|
|
|
def state_dir() -> pathlib.Path:
|
|
override = os.environ.get("CLAUDE_DISCORD_DIR")
|
|
if override:
|
|
return pathlib.Path(override)
|
|
return pathlib.Path.home() / ".claude-discord"
|
|
|
|
|
|
def hosts_file() -> pathlib.Path:
|
|
return state_dir() / "infra-hosts.json"
|
|
|
|
|
|
def log_file() -> pathlib.Path:
|
|
return state_dir() / "logs" / "infra.log"
|
|
|
|
|
|
def load_hosts() -> dict[str, dict]:
|
|
"""Lista de hosturi: fisierul de pe disc daca exista, altfel cea implicita."""
|
|
path = hosts_file()
|
|
if not path.is_file():
|
|
return dict(DEFAULT_HOSTS)
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc:
|
|
raise SystemExit(_fail(EXIT_CONFIG, f"infra: {path} nu e JSON valid ({exc})"))
|
|
if not isinstance(data, dict) or not data:
|
|
raise SystemExit(_fail(EXIT_CONFIG, f"infra: {path} nu contine hosturi"))
|
|
out: dict[str, dict] = {}
|
|
for name, spec in data.items():
|
|
if isinstance(spec, str):
|
|
spec = {"addr": spec}
|
|
if not isinstance(spec, dict) or not spec.get("addr"):
|
|
raise SystemExit(_fail(EXIT_CONFIG, f"infra: intrare invalida pentru '{name}'"))
|
|
out[str(name)] = {
|
|
"addr": str(spec["addr"]),
|
|
"user": str(spec.get("user") or "root"),
|
|
"prod": bool(spec.get("prod", False)),
|
|
"desc": str(spec.get("desc") or ""),
|
|
}
|
|
return out
|
|
|
|
|
|
def _fail(code: int, msg: str) -> int:
|
|
sys.stderr.write(msg + "\n")
|
|
return code
|
|
|
|
|
|
def log(host: str, target: str, cmd: list[str], rc, dur_s: float, note: str = "") -> None:
|
|
"""Jurnal pe o linie: data, host, comanda completa, rezultat."""
|
|
line = (
|
|
f"{datetime.datetime.now().isoformat(timespec='seconds')}\t"
|
|
f"host={host}\ttarget={target}\trc={rc}\tdur={dur_s:.2f}s\t"
|
|
f"cmd={shlex.join(cmd) if cmd else ''}"
|
|
)
|
|
if note:
|
|
line += f"\tnote={note}"
|
|
try:
|
|
path = log_file()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "a", encoding="utf-8") as fh:
|
|
fh.write(line + "\n")
|
|
except OSError:
|
|
pass # un jurnal care nu se poate scrie nu opreste comanda
|
|
|
|
|
|
def usage(hosts: dict[str, dict]) -> str:
|
|
lines = ["infra <host> <comanda...>", "", "Hosturi permise:"]
|
|
for name, spec in sorted(hosts.items()):
|
|
flag = " [PRODUCTIE]" if spec.get("prod") else ""
|
|
lines.append(f" {name:<13} {spec['user']}@{spec['addr']:<13} {spec.get('desc','')}{flag}")
|
|
lines.append("")
|
|
lines.append(f"Lista se poate inlocui prin {hosts_file()}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
hosts = load_hosts()
|
|
|
|
if not argv or argv[0] in ("-h", "--help"):
|
|
print(usage(hosts))
|
|
return 0 if argv else EXIT_USAGE
|
|
if argv[0] in ("-l", "--list"):
|
|
print(usage(hosts))
|
|
return 0
|
|
|
|
host = argv[0]
|
|
cmd = argv[1:]
|
|
|
|
if host not in hosts:
|
|
log(host, "-", cmd, "refuzat", 0.0, note="host in afara listei")
|
|
return _fail(
|
|
EXIT_UNKNOWN_HOST,
|
|
f"infra: host necunoscut '{host}'. Hosturi permise: "
|
|
+ ", ".join(sorted(hosts)),
|
|
)
|
|
if not cmd:
|
|
return _fail(EXIT_USAGE, f"infra: lipseste comanda pentru '{host}'")
|
|
|
|
spec = hosts[host]
|
|
target = f"{spec['user']}@{spec['addr']}"
|
|
ssh_argv = ["ssh", *SSH_OPTS, target, "--", *cmd]
|
|
|
|
if os.environ.get("INFRA_DRY_RUN"):
|
|
log(host, target, cmd, "dry-run", 0.0, note="INFRA_DRY_RUN")
|
|
print(shlex.join(ssh_argv))
|
|
return 0
|
|
|
|
t0 = time.monotonic()
|
|
try:
|
|
proc = subprocess.run(ssh_argv)
|
|
rc = proc.returncode
|
|
except FileNotFoundError:
|
|
log(host, target, cmd, "eroare", time.monotonic() - t0, note="ssh lipseste")
|
|
return _fail(EXIT_CONFIG, "infra: `ssh` nu exista in PATH")
|
|
except KeyboardInterrupt:
|
|
log(host, target, cmd, "intrerupt", time.monotonic() - t0)
|
|
return 130
|
|
log(host, target, cmd, rc, time.monotonic() - t0)
|
|
return rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main(sys.argv[1:]))
|
|
except SystemExit:
|
|
raise
|
|
except Exception as exc: # nimic nu iese neraportat
|
|
sys.stderr.write(f"infra: eroare interna: {exc}\n")
|
|
sys.exit(1)
|