feat(agent): агент логов — фильтр, LLM-страж, планировщик grep (#18)
CI / ruff · mypy · pytest (pull_request) Successful in 39m13s

- prefilter: правила из config/agent_rules.yaml (длина, команды, shell,
  секреты и системные файлы, prompt injection) — без LLM
- guard: отдельный вызов, JSON {verdict, reason}; невалидный ответ = attack,
  недоступный LLM — «попробуйте позже», а не атака
- planner: только AgentLogQuery (контейнер из белого списка, ≤ 7 д, шаблон
  ≤ 200 без вложенных квантификаторов, лимит ≤ 500); логи в LLM не идут
- бот: «🤖 Спросить агента» → показывает построенный grep и строки;
  атака — вежливый отказ и 🛡 администраторам; квота 30 в сутки
- журнал agent_requests + log_queries (mode=agent)
- при LLM_PROVIDER=mock агент выключен

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 15:02:07 +03:00
co-authored by Claude Opus 5.5
parent f89338e015
commit 65acf3abf1
20 changed files with 917 additions and 26 deletions
+12 -3
View File
@@ -9,6 +9,7 @@ from aiogram.client.session.aiohttp import AiohttpSession
from app import __version__
from app.access.service import AccessService
from app.agent.service import Agent
from app.bot.fsm import make_storage
from app.bot.middlewares.access import AccessMiddleware
from app.bot.routers import commit, common, logs, members, status
@@ -17,6 +18,7 @@ from app.db.maintenance import cleanup_loop
from app.db.migrate import upgrade
from app.db.session import SessionMaker, make_engine, make_sessionmaker
from app.health import heartbeat
from app.llm.factory import make_provider
from app.logs.journal import LogJournal
from app.logs_setup import setup_logging
from app.repo.drafts import DraftService
@@ -38,6 +40,7 @@ def build_dispatcher(
commit_url_template: str = "",
docker: DockerClient | None = None,
checker: Checker | None = None,
agent: Agent | None = None,
) -> Dispatcher:
dp = Dispatcher(storage=make_storage(sessions))
gate = AccessMiddleware(access, texts)
@@ -49,7 +52,7 @@ def build_dispatcher(
commit.build_router(texts, repo, DraftService(sessions), Publisher(repo, sessions), commit_url_template)
)
dp.include_router(status.build_router(texts, docker, checker))
dp.include_router(logs.build_router(texts, docker, LogJournal(sessions)))
dp.include_router(logs.build_router(texts, docker, LogJournal(sessions), agent, access))
dp.include_router(common.build_router(texts, access))
return dp
@@ -66,8 +69,11 @@ async def run(settings: Settings) -> None:
docker = DockerClient(settings.docker) if settings.docker.url else None
store = SnapshotStore(sessions)
checker = Checker(docker, store, checks_cfg) if docker else None
# The mock provider answers `{}` to the guard — every question would be an «attack»: no real LLM, no agent
llm = make_provider(settings.llm) if settings.llm.provider != "mock" else None
agent = Agent.from_config(llm, sessions, settings.runtime.config_dir) if llm else None
dp = build_dispatcher(
texts, sessions, access, UploadsRepo(settings.git), settings.git.commit_url_template, docker, checker
texts, sessions, access, UploadsRepo(settings.git), settings.git.commit_url_template, docker, checker, agent
)
rt = settings.runtime
background = [
@@ -80,11 +86,12 @@ async def run(settings: Settings) -> None:
session = AiohttpSession(proxy=proxy.get_secret_value()) if proxy else None
bot = Bot(settings.telegram.bot_token.get_secret_value(), session=session)
log.info(
"tgit %s starting, %d bootstrap admin(s), telegram %s, docker %s",
"tgit %s starting, %d bootstrap admin(s), telegram %s, docker %s, agent %s",
__version__,
len(settings.access.admin_ids),
"via proxy" if proxy else "direct",
"on" if docker else "off",
settings.llm.provider if agent else "off",
)
try:
await dp.start_polling(bot, handle_signals=True)
@@ -99,6 +106,8 @@ async def run(settings: Settings) -> None:
await docker.aclose()
if checker:
await checker.aclose()
if llm:
await llm.aclose()
await engine.dispose()
log.info("tgit stopped")