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>
54 lines
1.5 KiB
Python
54 lines
1.5 KiB
Python
"""Step 1 (SPEC L-10): deterministic rules from `config/agent_rules.yaml`, before any LLM call."""
|
|
|
|
from pathlib import Path
|
|
|
|
import regex
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
class Rule(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
reason: str
|
|
pattern: str
|
|
|
|
@field_validator("pattern")
|
|
@classmethod
|
|
def _compiles(cls, value: str) -> str:
|
|
try:
|
|
regex.compile(value)
|
|
except regex.error as e:
|
|
raise ValueError(f"bad pattern: {e}") from e
|
|
return value
|
|
|
|
|
|
class AgentRules(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
max_length: int = Field(default=500, gt=0)
|
|
rules: list[Rule]
|
|
|
|
|
|
def load_rules(config_dir: Path) -> AgentRules:
|
|
with (config_dir / "agent_rules.yaml").open(encoding="utf-8") as f:
|
|
return AgentRules.model_validate(yaml.safe_load(f))
|
|
|
|
|
|
TOO_LONG = "слишком длинный запрос"
|
|
|
|
|
|
class Prefilter:
|
|
def __init__(self, rules: AgentRules) -> None:
|
|
self._max = rules.max_length
|
|
self._rules = [(r.reason, regex.compile(r.pattern, regex.IGNORECASE)) for r in rules.rules]
|
|
|
|
def check(self, prompt: str) -> str | None:
|
|
"""The reason to refuse, or None when the prompt may go on to the guard."""
|
|
if len(prompt) > self._max:
|
|
return TOO_LONG
|
|
for reason, pattern in self._rules:
|
|
if pattern.search(prompt, timeout=0.1):
|
|
return reason
|
|
return None
|