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:
Claude Agent
2026-08-30 10:44:39 +00:00
parent d7b4007af8
commit d466f358ce
43 changed files with 7767 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
"""T6: parser tolerant."""
from __future__ import annotations
import json
import pytest
import stream
def P(**kw):
return stream.StreamParser(**kw)
def test_system_init_aduce_session_id():
p = P()
(ev,) = p.feed_line(json.dumps({"type": "system", "subtype": "init",
"session_id": "s1", "model": "sonnet", "cwd": "/workspace",
"tools": ["Bash"]}))
assert isinstance(ev, stream.SystemInit)
assert ev.session_id == "s1" and ev.model == "sonnet" and ev.tools == ("Bash",)
def test_assistant_text_si_tool_use():
p = P()
evs = p.feed_line(json.dumps({"type": "assistant", "session_id": "s1", "message": {"content": [
{"type": "text", "text": "salut"},
{"type": "tool_use", "id": "tu1", "name": "Bash", "input": {"command": "ls"}}]}}))
assert [type(e) for e in evs] == [stream.AssistantText, stream.ToolUse]
assert evs[0].text == "salut"
assert evs[1].name == "Bash" and evs[1].input["command"] == "ls"
def test_tool_result_din_user():
p = P()
(ev,) = p.feed_line(json.dumps({"type": "user", "message": {"content": [
{"type": "tool_result", "tool_use_id": "tu1", "content": [{"type": "text", "text": "out"}]}]}}))
assert isinstance(ev, stream.ToolResult) and ev.text == "out" and ev.is_error is False
def test_result_aduce_cost_si_durata():
p = P()
(ev,) = p.feed_line(json.dumps({"type": "result", "subtype": "success", "is_error": False,
"total_cost_usd": 0.1547, "duration_ms": 34500,
"num_turns": 3, "result": "gata"}))
assert (ev.total_cost_usd, ev.duration_ms, ev.num_turns, ev.text) == (0.1547, 34500, 3, "gata")
assert ev.is_error is False and p.saw_result
def test_result_cu_campuri_stricate_nu_crapa():
p = P()
(ev,) = p.feed_line(json.dumps({"type": "result", "total_cost_usd": "nu-i numar",
"duration_ms": None, "num_turns": "x"}))
assert ev.total_cost_usd == 0.0 and ev.duration_ms == 0 and ev.num_turns == 0
def test_result_de_eroare_marcat():
p = P()
(ev,) = p.feed_line(json.dumps({"type": "result", "subtype": "error_during_execution"}))
assert ev.is_error is True
def test_tip_necunoscut_logat_o_singura_data():
calls = []
p = P(version_fn=lambda: (calls.append(1), "2.1.251")[1])
line = json.dumps({"type": "rate_limit_event", "detail": "x"})
assert p.feed_line(line) == []
assert p.feed_line(line) == []
assert p.feed_line(json.dumps({"type": "alt_tip_nou"})) == []
assert p.unknown_types == {"rate_limit_event", "alt_tip_nou"}
assert len(calls) == 2 # o data per tip, nu per linie
def test_linie_non_json_ignorata(caplog):
p = P()
with caplog.at_level("WARNING"):
assert p.feed_line("Error: something went wrong") == []
assert p.feed_line("[1, 2, 3]") == []
assert p.bad_lines == 2
assert any("non-JSON" in r.message or "nu e obiect" in r.message for r in caplog.records)
def test_linii_goale_ignorate():
p = P()
assert p.feed_line("") == [] and p.feed_line(" \n") == []
async def _aiter(lines):
for x in lines:
yield x
async def test_eof_inainte_de_result_da_eroare_explicita():
p = P()
got = []
with pytest.raises(stream.StreamEOFError):
async for ev in p.aiter_events(_aiter([
json.dumps({"type": "system", "subtype": "init", "session_id": "s1"}),
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "hm"}]}}),
])):
got.append(ev)
assert len(got) == 2
async def test_aiter_se_opreste_la_result():
p = P()
evs = [e async for e in p.aiter_events(_aiter([
json.dumps({"type": "system", "subtype": "init", "session_id": "s1"}),
json.dumps({"type": "result", "total_cost_usd": 1.0}),
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "dupa"}]}}),
]))]
assert [type(e) for e in evs] == [stream.SystemInit, stream.Result]
async def test_aiter_accepta_bytes_si_gunoi():
p = P()
evs = [e async for e in p.aiter_events(_aiter([
b"gunoi binar\n",
b'{"type":"result","total_cost_usd":0.5}\n',
]))]
assert len(evs) == 1 and evs[0].total_cost_usd == 0.5