""" iss-agent-kit: комплект подключения ИИ-агента к контуру допустимости действий и ГОСТ-журналу (ИБ 3.0). Тонкий фасад поверх mcp-sentinel (проверка вызовов и результатов инструментов) и iss-ledger-117 (неизменяемый криптографический журнал ГОСТ Р 34.11-2012): один класс AgentGuard и один импорт вместо ручной сборки MCPSentinel + IssLedger117 в каждом агенте. """ from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional, Tuple from mcp_sentinel import MCPSentinel from iss_ledger_117 import IssLedger117 __version__ = "0.1.1" __all__ = [ "ActionBlocked", "Verdict", "AgentGuard", "MCPSentinel", "IssLedger117", "__version__", ] class ActionBlocked(Exception): """ Действие агента заблокировано инвариантом безопасности. Атрибуты: - reason: обоснование вердикта (причина отказа) - details: подробности проверки контракта, если есть - stage: "call" -- заблокирован сам вызов инструмента (fn НЕ выполнялась) "result" -- заблокирован результат выполнения (fn уже выполнилась) """ def __init__(self, reason: str, details: Optional[dict] = None, stage: str = "call"): self.reason = reason self.details = details self.stage = stage super().__init__(reason) @dataclass class Verdict: """Результат проверки вызова инструмента контуром допустимости (без исполнения).""" allowed: bool reason: str details: Optional[dict] = None class AgentGuard: """ Фасад для подключения ИИ-агента к контуру допустимости действий (mcp-sentinel) и криптографическому журналу (iss-ledger-117) в три строки: from iss_agent_kit import AgentGuard guard = AgentGuard(ledger_path="audit_gost.jsonl") result = guard.run(execute_sql, "sql_query", {"query": "SELECT 1"}) """ def __init__( self, ledger_path: Optional[str] = "audit_gost.jsonl", allowed_roots: Optional[List[str]] = None, contracts: Optional[list] = None, agent_id: str = "ai-agent", ): self.agent_id = agent_id self._sentinel = MCPSentinel( ledger_path=ledger_path, allowed_roots=allowed_roots, contracts=contracts, ) def check(self, tool_name: str, arguments: dict, agent_id: Optional[str] = None) -> Verdict: """Только проверка вызова + запись вердикта в журнал. Инструмент НЕ вызывается.""" allowed, reason, details = self._sentinel.validate_tool_call( agent_id=agent_id or self.agent_id, tool_name=tool_name, arguments=arguments, ) return Verdict(allowed=allowed, reason=reason, details=details) def run( self, fn: Callable[..., Any], tool_name: str, arguments: dict, agent_id: Optional[str] = None, ) -> Any: """ check() -> при BLOCK бросает ActionBlocked(stage="call"), fn НЕ вызывается. Иначе: result = fn(**arguments); затем проверка результата (validate_tool_result(str(result))) -> при BLOCK бросает ActionBlocked(stage="result"). Иначе возвращает result. """ effective_agent = agent_id or self.agent_id verdict = self.check(tool_name, arguments, agent_id=effective_agent) if not verdict.allowed: raise ActionBlocked(verdict.reason, verdict.details, stage="call") result = fn(**arguments) allowed, reason = self._sentinel.validate_tool_result(effective_agent, tool_name, str(result)) if not allowed: raise ActionBlocked(reason, None, stage="result") return result def tool(self, tool_name: str): """ Декоратор: оборачивает функцию так, что вызов wrapped(**kwargs) идёт через run(). @guard.tool("sql_query") def sql_query(query: str): return db.execute(query) sql_query(query="SELECT 1") # проходит через AgentGuard.run() """ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: def wrapped(**kwargs): return self.run(fn, tool_name, kwargs) wrapped.__name__ = getattr(fn, "__name__", tool_name) wrapped.__doc__ = fn.__doc__ return wrapped return decorator def verify_ledger(self) -> Tuple[bool, int, str]: """Проксирует IssLedger117.verify(). Если журнал отключён (ledger_path=None) -- (True, 0, "ledger disabled").""" if self._sentinel.ledger is None: return True, 0, "ledger disabled" return self._sentinel.ledger.verify()