feat(agent): агент логов — фильтр, LLM-страж, планировщик grep (#18)
CI / ruff · mypy · pytest (pull_request) Successful in 39m13s
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:
@@ -0,0 +1,38 @@
|
||||
"""Step 2 (SPEC L-11): a separate, narrow LLM call — is the question an attack? Anything but a valid `safe` is one."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.db.models import Verdict
|
||||
from app.llm.provider import ChatMessage, JsonSchema, LLMProvider
|
||||
|
||||
SCHEMA = JsonSchema(
|
||||
name="guard_verdict",
|
||||
schema={
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["verdict", "reason"],
|
||||
"properties": {
|
||||
"verdict": {"type": "string", "enum": ["safe", "attack"]},
|
||||
"reason": {"type": "string"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
INVALID = "страж вернул не JSON по схеме"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GuardVerdict:
|
||||
verdict: Verdict
|
||||
reason: str
|
||||
tokens: int
|
||||
|
||||
|
||||
async def classify(llm: LLMProvider, system_prompt: str, prompt: str) -> GuardVerdict:
|
||||
"""LLMError propagates: an unreachable model is «try later», not an attack."""
|
||||
result = await llm.complete([ChatMessage("system", system_prompt), ChatMessage("user", prompt)], SCHEMA)
|
||||
data = result.data or {}
|
||||
verdict, reason = data.get("verdict"), data.get("reason")
|
||||
if verdict not in ("safe", "attack") or not isinstance(reason, str):
|
||||
return GuardVerdict(Verdict.ATTACK, INVALID, result.tokens)
|
||||
return GuardVerdict(Verdict(verdict), reason[:300], result.tokens)
|
||||
Reference in New Issue
Block a user