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:
122
proxmox/lxc171-claude-agent/discord-bridge/tests/fake_claude.py
Executable file
122
proxmox/lxc171-claude-agent/discord-bridge/tests/fake_claude.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI `claude` fals pentru teste: citeste JSONL pe stdin, emite JSONL pe stdout.
|
||||
|
||||
Scenariul se alege din FAKE_CLAUDE_SCENARIO:
|
||||
normal assistant + result pentru fiecare mesaj
|
||||
unknown tip necunoscut + linie non-JSON inainte de assistant/result
|
||||
tools tool_use + tool_result + text + result
|
||||
eof se termina inainte de `result`
|
||||
crash scrie pe stderr si iese cu cod 1
|
||||
env raporteaza CLAUDE_DISCORD_THREAD_ID / _SESSION_ID din mediul propriu
|
||||
slow "lucreaza" FAKE_CLAUDE_DELAY secunde, colecteaza mesajele venite intre timp
|
||||
(asa se testeaza steering-ul mid-tur) si le enumera in `result`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def emit(obj) -> None:
|
||||
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
sid, model = None, "sonnet"
|
||||
for i, a in enumerate(argv):
|
||||
if a == "--resume" and i + 1 < len(argv):
|
||||
sid = argv[i + 1]
|
||||
elif a == "--model" and i + 1 < len(argv):
|
||||
model = argv[i + 1]
|
||||
return sid, model
|
||||
|
||||
|
||||
def text_of(line: str) -> str:
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except ValueError:
|
||||
return line.strip()
|
||||
msg = obj.get("message") or {}
|
||||
content = msg.get("content")
|
||||
if isinstance(content, list):
|
||||
return "".join(b.get("text", "") for b in content if isinstance(b, dict))
|
||||
return str(content or "")
|
||||
|
||||
|
||||
def assistant(sid: str, text: str) -> None:
|
||||
emit({"type": "assistant", "session_id": sid,
|
||||
"message": {"role": "assistant", "content": [{"type": "text", "text": text}]}})
|
||||
|
||||
|
||||
def result(sid: str, text: str, ms: int = 10) -> None:
|
||||
emit({"type": "result", "subtype": "success", "session_id": sid, "is_error": False,
|
||||
"duration_ms": ms, "num_turns": 1, "result": text,
|
||||
"total_cost_usd": float(os.environ.get("FAKE_CLAUDE_COST", "0.0123"))})
|
||||
|
||||
|
||||
def drain(deadline: float) -> list[str]:
|
||||
"""Citeste orice mesaj suplimentar pana la deadline (steering mid-tur)."""
|
||||
extra = []
|
||||
while True:
|
||||
left = deadline - time.monotonic()
|
||||
if left <= 0:
|
||||
return extra
|
||||
r, _, _ = select.select([sys.stdin], [], [], min(left, 0.05))
|
||||
if r:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
return extra
|
||||
extra.append(text_of(line))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
scenario = os.environ.get("FAKE_CLAUDE_SCENARIO", "normal")
|
||||
sid, model = parse_args(sys.argv[1:])
|
||||
sid = sid or os.environ.get("FAKE_CLAUDE_SID", "sid-fake-0001")
|
||||
emit({"type": "system", "subtype": "init", "session_id": sid, "model": model,
|
||||
"cwd": os.getcwd(), "tools": ["Bash", "Read"]})
|
||||
if scenario == "crash":
|
||||
sys.stderr.write("fake_claude: boom, ies cu 1\n")
|
||||
sys.stderr.flush()
|
||||
return 1
|
||||
|
||||
for line in sys.stdin:
|
||||
prompt = text_of(line)
|
||||
if scenario == "unknown":
|
||||
emit({"type": "rate_limit_event", "session_id": sid, "detail": "test"})
|
||||
sys.stdout.write("asta nu e JSON\n")
|
||||
sys.stdout.flush()
|
||||
if scenario == "tools":
|
||||
emit({"type": "assistant", "session_id": sid, "message": {"role": "assistant", "content": [
|
||||
{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}]}})
|
||||
emit({"type": "user", "session_id": sid, "message": {"role": "user", "content": [
|
||||
{"type": "tool_result", "tool_use_id": "tu_1", "content": "fisier1\n"}]}})
|
||||
if scenario == "eof":
|
||||
assistant(sid, "incep si mor")
|
||||
return 0
|
||||
if scenario == "slow":
|
||||
delay = float(os.environ.get("FAKE_CLAUDE_DELAY", "1.0"))
|
||||
assistant(sid, "lucrez...")
|
||||
extra = drain(time.monotonic() + delay)
|
||||
msgs = [prompt] + extra
|
||||
result(sid, "mesaje primite: " + " | ".join(msgs), int(delay * 1000))
|
||||
continue
|
||||
if scenario == "env":
|
||||
assistant(sid, "raportez mediul")
|
||||
result(sid, "thread={} sesiune={} marker={}".format(
|
||||
os.environ.get("CLAUDE_DISCORD_THREAD_ID", "-"),
|
||||
os.environ.get("CLAUDE_DISCORD_SESSION_ID", "-"),
|
||||
os.environ.get("MARKER_DE_TEST", "-")))
|
||||
continue
|
||||
assistant(sid, f"ecou: {prompt}")
|
||||
result(sid, f"ecou: {prompt}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user