"""Rate limiting: per agent or per tool; log advisory or reject if exceeded. In ADVISORY mode, rate limit violations are logged as advisories but the action proceeds. Growth requires freedom to push limits. """ import time from collections import defaultdict from fusionagi._logger import logger from fusionagi.schemas.audit import GovernanceMode class RateLimiter: """Simple in-memory rate limiter: max N calls per window_seconds per key. In ADVISORY mode (default), exceeded limits are logged but not enforced. """ def __init__( self, max_calls: int = 60, window_seconds: float = 60.0, mode: GovernanceMode = GovernanceMode.ADVISORY, ) -> None: self._max_calls = max_calls self._window = window_seconds self._calls: dict[str, list[float]] = defaultdict(list) self._mode = mode def allow(self, key: str) -> tuple[bool, str]: """Record a call for key; return (True, "") or (False/True, reason).""" now = time.monotonic() cutoff = now - self._window self._calls[key] = [t for t in self._calls[key] if t > cutoff] if len(self._calls[key]) >= self._max_calls: reason = f"Rate limit exceeded for {key}" if self._mode == GovernanceMode.ADVISORY: logger.info( "RateLimiter advisory: limit exceeded (proceeding)", extra={"key": key, "reason": reason, "mode": "advisory"}, ) self._calls[key].append(now) return True, f"Advisory: {reason}" logger.info("Rate limiter rejected", extra={"key": key, "reason": reason}) return False, reason self._calls[key].append(now) return True, "" def reset(self, key: str | None = None) -> None: """Reset counts for key or all.""" if key is None: self._calls.clear() else: self._calls.pop(key, None)