feat(discord-bridge): punte Discord -> Claude Code pe LXC 171
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
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Unit systemd de UTILIZATOR pentru puntea Discord -> Claude Code (LXC 171).
|
||||
#
|
||||
# Se instaleaza in ~/.config/systemd/user/claude-discord.service (vezi install.sh).
|
||||
# Rulare ca utilizatorul `claude`, NU ca root: puntea are exact accesul pe care il
|
||||
# are omul la terminal — decizie de arhitectura, nu scapare (vezi README, Securitate).
|
||||
#
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now claude-discord
|
||||
# systemctl --user status claude-discord
|
||||
# journalctl --user -u claude-discord -f
|
||||
|
||||
[Unit]
|
||||
Description=Punte Discord -> Claude Code (LXC 171 claude-agent)
|
||||
Documentation=file:///workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge/README.md
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
# Crash loop: dupa 5 porniri esuate in 300s systemd renunta si lasa unitul in
|
||||
# `failed`, in loc sa se invarta la nesfarsit. Botul trimite alerta pe email
|
||||
# inainte sa moara (alerts.alert_crash_loop).
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/.claude-discord
|
||||
|
||||
# Token-ul Discord si allowlist-ul stau in fisier 0600, nu in unit.
|
||||
EnvironmentFile=%h/.claude-discord/env
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
# CLI-ul `claude` e instalat prin nvm; PATH-ul unui unit de utilizator nu-l contine.
|
||||
Environment=PATH=%h/.nvm/versions/node/v20.19.6/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
ExecStartPre=/bin/mkdir -p %h/.claude-discord/logs
|
||||
ExecStart=%h/.claude-discord/venv/bin/python /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge/bot.py
|
||||
|
||||
# ESENTIAL: la stop/restart omoara TOT cgroup-ul, nu doar procesul principal.
|
||||
# Fara asta procesele `claude` (406 MB RSS masurat) raman in urma si umplu
|
||||
# containerul. Nu prinde totusi ce s-a desprins din cgroup — pentru aia exista
|
||||
# comanda `!cleanup` (cleanup.py).
|
||||
KillMode=control-group
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=30
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
# Backoff exponential: 5s, 10s, 20s, ... plafonat la 120s (systemd >= 254).
|
||||
RestartSteps=5
|
||||
RestartMaxDelaySec=120
|
||||
|
||||
# Containerul are istoric de OOM (incident 2026-06-24). MemoryHigh strange
|
||||
# procesul prin reclaim inainte sa se ajunga la OOM-kill la MemoryMax.
|
||||
MemoryHigh=3G
|
||||
MemoryMax=6G
|
||||
|
||||
StandardOutput=append:%h/.claude-discord/logs/bot.log
|
||||
StandardError=append:%h/.claude-discord/logs/bot.log
|
||||
SyslogIdentifier=claude-discord
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
45
proxmox/lxc171-claude-agent/discord-bridge/ops/env.example
Normal file
45
proxmox/lxc171-claude-agent/discord-bridge/ops/env.example
Normal file
@@ -0,0 +1,45 @@
|
||||
# Configurarea puntii Discord -> Claude Code.
|
||||
#
|
||||
# install.sh copiaza acest fisier in ~/.claude-discord/env cu drepturi 0600.
|
||||
# Formatul e cel cerut de systemd EnvironmentFile: CHEIE=valoare, fara `export`,
|
||||
# fara ghilimele in jurul valorii, fara expandare de variabile.
|
||||
#
|
||||
# DUPA COPIERE COMPLETEAZA MANUAL cel putin DISCORD_TOKEN si allowlist-ul.
|
||||
|
||||
# --- Discord ---------------------------------------------------------------
|
||||
# Token-ul botului: Discord Developer Portal -> aplicatia ta -> Bot -> Reset Token.
|
||||
# Se vede o singura data. Daca ajunge in git, invalideaza-l imediat din portal.
|
||||
DISCORD_TOKEN=
|
||||
|
||||
# Allowlist. Liste separate prin virgula, cu ID-uri numerice (Discord -> Advanced ->
|
||||
# Developer Mode, apoi click dreapta -> Copy ID). Gol = nimic permis (fail-closed).
|
||||
DISCORD_GUILD_IDS=
|
||||
DISCORD_CHANNEL_IDS=
|
||||
DISCORD_USER_IDS=
|
||||
|
||||
# --- Model si cost ---------------------------------------------------------
|
||||
# Modelul implicit al firelor noi. `!model opus` il schimba per fir.
|
||||
CLAUDE_MODEL=sonnet
|
||||
# Plafon de cost pe zi, in USD. La atingere botul refuza tururi noi si alerteaza.
|
||||
# Reper masurat: un tur banal pe opus a costat $0.1547.
|
||||
COST_CAP_USD_DAY=5.00
|
||||
|
||||
# --- Limite ----------------------------------------------------------------
|
||||
# Maxim de procese `claude` vii simultan (406 MB RSS fiecare, masurat).
|
||||
MAX_LIVE_PROCESSES=4
|
||||
# Timeout per tur, secunde.
|
||||
TURN_TIMEOUT_S=900
|
||||
# Reaper: dupa cate secunde de inactivitate se opreste procesul unui fir.
|
||||
IDLE_REAP_S=1200
|
||||
# Cat asteapta hook-ul de confirmare o apasare pe Allow/Deny, secunde.
|
||||
# CONSTRANGERE: trebuie sa ramana MAI MIC decat `timeout` din bot-settings.json
|
||||
# (acum 330), altfel CLI-ul taie hook-ul inainte ca acesta sa refuze curat.
|
||||
CLAUDE_DISCORD_APPROVAL_TIMEOUT=300
|
||||
|
||||
# --- Alerte (alerts.py) ----------------------------------------------------
|
||||
# Destinatarul emailurilor de alerta. Aceeasi conventie ca restul repo-ului
|
||||
# (vezi proxmox/vm109-windows-dr/scripts/pveelite-down-alert.sh): default root.
|
||||
ALERT_RECIPIENT=root
|
||||
|
||||
# --- Directorul de lucru implicit -----------------------------------------
|
||||
DEFAULT_CWD=/workspace
|
||||
151
proxmox/lxc171-claude-agent/discord-bridge/ops/install.sh
Executable file
151
proxmox/lxc171-claude-agent/discord-bridge/ops/install.sh
Executable file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — instaleaza puntea Discord -> Claude Code pe LXC 171 (claude-agent).
|
||||
#
|
||||
# IDEMPOTENT: se poate rula de cate ori vrei. Nu suprascrie niciodata
|
||||
# ~/.claude-discord/env (acolo sta token-ul completat de om).
|
||||
#
|
||||
# ./ops/install.sh # instaleaza / actualizeaza, NU porneste serviciul
|
||||
# ./ops/install.sh --start # in plus porneste serviciul (cere DISCORD_TOKEN completat)
|
||||
#
|
||||
# Ce face:
|
||||
# 1. ~/.claude-discord/ cu 0700 si logs/
|
||||
# 2. env 0600 din ops/env.example (doar daca lipseste)
|
||||
# 3. venv + dependinte din requirements.txt
|
||||
# 4. loginctl enable-linger (serviciul de utilizator trebuie sa supravietuiasca logout-ului)
|
||||
# 5. symlink unit -> ~/.config/systemd/user/claude-discord.service
|
||||
# 6. intrare de crontab pentru logrotate
|
||||
# 7. systemd-analyze verify + enable
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SRC="$(dirname "$HERE")" # .../discord-bridge
|
||||
STATE_DIR="$HOME/.claude-discord"
|
||||
UNIT_DIR="$HOME/.config/systemd/user"
|
||||
UNIT_NAME="claude-discord.service"
|
||||
DO_START=0
|
||||
[ "${1:-}" = "--start" ] && DO_START=1
|
||||
|
||||
info() { printf ' \033[32m*\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||
die() { printf ' \033[31mX\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] && die "NU rula ca root. Puntea e un serviciu de utilizator (claude)."
|
||||
|
||||
echo "== Punte Discord -> Claude Code: instalare =="
|
||||
echo " sursa: $SRC"
|
||||
|
||||
# --- 1. director de stare --------------------------------------------------
|
||||
mkdir -p "$STATE_DIR/logs" "$STATE_DIR/approvals"
|
||||
chmod 0700 "$STATE_DIR"
|
||||
info "director de stare: $STATE_DIR (0700)"
|
||||
|
||||
# --- 2. env ----------------------------------------------------------------
|
||||
if [ -f "$STATE_DIR/env" ]; then
|
||||
chmod 0600 "$STATE_DIR/env"
|
||||
info "env exista deja, nu se atinge"
|
||||
else
|
||||
cp "$HERE/env.example" "$STATE_DIR/env"
|
||||
chmod 0600 "$STATE_DIR/env"
|
||||
info "env creat din env.example (0600) — COMPLETEAZA-L MANUAL"
|
||||
fi
|
||||
if ! grep -qE '^DISCORD_TOKEN=.+' "$STATE_DIR/env" 2>/dev/null; then
|
||||
warn "DISCORD_TOKEN e gol in $STATE_DIR/env — botul nu poate porni"
|
||||
fi
|
||||
# Sunt fisiere versionate langa cod: env-ul real NU trebuie sa ajunga in git.
|
||||
if [ -e "$SRC/env" ]; then
|
||||
warn "exista $SRC/env in arborele de cod — muta-l in $STATE_DIR/env si sterge-l"
|
||||
fi
|
||||
|
||||
# --- 3. venv ---------------------------------------------------------------
|
||||
if [ ! -x "$STATE_DIR/venv/bin/python" ]; then
|
||||
python3 -m venv "$STATE_DIR/venv" || die "crearea venv a esuat"
|
||||
info "venv creat: $STATE_DIR/venv"
|
||||
else
|
||||
info "venv exista deja"
|
||||
fi
|
||||
"$STATE_DIR/venv/bin/python" -m pip install --quiet --upgrade pip >/dev/null 2>&1
|
||||
if [ -f "$SRC/requirements.txt" ]; then
|
||||
if "$STATE_DIR/venv/bin/python" -m pip install --quiet -r "$SRC/requirements.txt"; then
|
||||
info "dependinte instalate din requirements.txt"
|
||||
else
|
||||
warn "instalarea dependintelor a esuat (retea?) — reia cu: $STATE_DIR/venv/bin/pip install -r $SRC/requirements.txt"
|
||||
fi
|
||||
else
|
||||
warn "requirements.txt lipseste inca (il aduce Lane A) — sari peste dependinte"
|
||||
fi
|
||||
|
||||
# --- 4. linger -------------------------------------------------------------
|
||||
# Fara linger, serviciul de utilizator moare la ultimul logout si nu porneste la boot.
|
||||
if [ "$(loginctl show-user "$USER" -p Linger --value 2>/dev/null)" = "yes" ]; then
|
||||
info "linger deja activ pentru $USER"
|
||||
else
|
||||
if loginctl enable-linger "$USER" 2>/dev/null; then
|
||||
info "linger activat pentru $USER"
|
||||
elif sudo -n loginctl enable-linger "$USER" 2>/dev/null; then
|
||||
info "linger activat pentru $USER (prin sudo)"
|
||||
else
|
||||
warn "nu am putut activa linger; ruleaza manual: sudo loginctl enable-linger $USER"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 5. unit ---------------------------------------------------------------
|
||||
mkdir -p "$UNIT_DIR"
|
||||
# symlink: unitul ramane versionat in repo, actualizarile vin cu `git pull`
|
||||
ln -sfn "$HERE/$UNIT_NAME" "$UNIT_DIR/$UNIT_NAME"
|
||||
info "unit legat: $UNIT_DIR/$UNIT_NAME -> $HERE/$UNIT_NAME"
|
||||
systemctl --user daemon-reload 2>/dev/null || warn "daemon-reload a esuat (sesiune fara systemd de utilizator?)"
|
||||
|
||||
if systemd-analyze verify "$UNIT_DIR/$UNIT_NAME" 2>&1 | grep -vE 'Unknown key|^$' | grep -q .; then
|
||||
systemd-analyze verify "$UNIT_DIR/$UNIT_NAME" 2>&1 | sed 's/^/ /'
|
||||
warn "systemd-analyze verify a raportat probleme (vezi mai sus)"
|
||||
else
|
||||
info "systemd-analyze verify: curat"
|
||||
fi
|
||||
|
||||
# --- 6. logrotate ----------------------------------------------------------
|
||||
CRON_LINE="10 4 * * * /usr/sbin/logrotate -s $STATE_DIR/logrotate.state $HERE/logrotate.conf"
|
||||
if crontab -l 2>/dev/null | grep -Fq "$HERE/logrotate.conf"; then
|
||||
info "crontab logrotate exista deja"
|
||||
else
|
||||
# Prin fisier temporar, nu prin pipe: cu `pipefail` un SIGPIPE de la `crontab -`
|
||||
# ar face pasul sa para esuat chiar cand a reusit.
|
||||
TMP_CRON="$(mktemp)"
|
||||
crontab -l 2>/dev/null > "$TMP_CRON"
|
||||
echo "$CRON_LINE" >> "$TMP_CRON"
|
||||
if crontab "$TMP_CRON"; then
|
||||
info "crontab logrotate adaugat (zilnic 04:10)"
|
||||
else
|
||||
warn "nu am putut scrie crontab-ul; adauga manual: $CRON_LINE"
|
||||
fi
|
||||
rm -f "$TMP_CRON"
|
||||
fi
|
||||
|
||||
# --- 7. mail ---------------------------------------------------------------
|
||||
if command -v mail >/dev/null 2>&1; then
|
||||
info "binarul mail: $(command -v mail)"
|
||||
else
|
||||
warn "binarul \`mail\` lipseste — alertele se degradeaza la scriere in log."
|
||||
warn "instaleaza-l cu: sudo apt-get install -y bsd-mailx"
|
||||
fi
|
||||
|
||||
# --- 8. enable / start -----------------------------------------------------
|
||||
systemctl --user enable "$UNIT_NAME" >/dev/null 2>&1 \
|
||||
&& info "serviciu enabled (porneste la boot)" \
|
||||
|| warn "enable a esuat"
|
||||
|
||||
if [ "$DO_START" -eq 1 ]; then
|
||||
if [ ! -f "$SRC/bot.py" ]; then
|
||||
die "bot.py lipseste in $SRC — nu pornesc nimic"
|
||||
fi
|
||||
if ! grep -qE '^DISCORD_TOKEN=.+' "$STATE_DIR/env"; then
|
||||
die "DISCORD_TOKEN gol — completeaza $STATE_DIR/env inainte de --start"
|
||||
fi
|
||||
systemctl --user restart "$UNIT_NAME" && info "serviciu pornit"
|
||||
systemctl --user --no-pager status "$UNIT_NAME" | sed 's/^/ /'
|
||||
else
|
||||
echo
|
||||
echo " Serviciul NU a fost pornit (intentionat)."
|
||||
echo " Dupa ce completezi $STATE_DIR/env: $0 --start"
|
||||
fi
|
||||
|
||||
echo "== gata =="
|
||||
@@ -0,0 +1,26 @@
|
||||
# Rotatie pentru logurile puntii Discord -> Claude Code.
|
||||
#
|
||||
# Sunt loguri de UTILIZATOR (~/.claude-discord/logs/), nu /var/log, deci NU intra
|
||||
# in /etc/logrotate.d — logrotate-ul de sistem ruleaza ca root si ar schimba
|
||||
# proprietarul fisierelor. Se ruleaza cu starea in home-ul utilizatorului:
|
||||
#
|
||||
# logrotate -s ~/.claude-discord/logrotate.state \
|
||||
# /workspace/romfastsql/proxmox/lxc171-claude-agent/discord-bridge/ops/logrotate.conf
|
||||
#
|
||||
# install.sh il pune in crontab-ul utilizatorului, zilnic la 04:10.
|
||||
#
|
||||
# Nota: unitul scrie cu `append:`, adica tine descriptorul deschis. De aceea
|
||||
# `copytruncate` — fara el, dupa rotatie botul ar continua sa scrie in inodul
|
||||
# vechi si logul nou ar ramane gol.
|
||||
|
||||
/home/claude/.claude-discord/logs/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
size 20M
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 0640 claude claude
|
||||
}
|
||||
Reference in New Issue
Block a user