Skip to content

Rate limiting in PolicyEngine #39

@dgenio

Description

@dgenio

Milestone: v0.2.0 | Tier: Medium | Effort: Medium

Problem

security.md explicitly lists rate limiting as a gap: there is no throttling of any kind. A compromised or runaway agent can invoke capabilities at unlimited rate, potentially:

  • Exhausting downstream API quotas
  • Running up costs on pay-per-call services
  • Performing denial-of-service against tool backends
  • Extracting large volumes of sensitive data before detection

This is the #1 security gap identified in the repo's own documentation.

Proposed Change

1. Rate limiter implementation

Add a sliding-window rate limiter to DefaultPolicyEngine, enforced per (principal_id, capability_id) pair:

class RateLimiter:
    """Sliding-window rate limiter using monotonic clock."""
    
    def check(self, key: str, limit: int, window_seconds: float) -> bool: ...
    def record(self, key: str) -> None: ...

2. Default rate limits by safety class

Safety Class Default Limit Window
READ 60 invocations 60 seconds
WRITE 10 invocations 60 seconds
DESTRUCTIVE 2 invocations 60 seconds

3. Configuration

  • Configurable via DefaultPolicyEngine constructor: rate_limits: dict[SafetyClass, tuple[int, float]]
  • Per-capability overrides via capability metadata or constraints
  • "service" role principals get 10x the default limits

4. Integration point

  • Check rate limit in evaluate() after role/sensitivity checks pass, before returning PolicyDecision.
  • On limit exceeded: raise PolicyDenied(f"Rate limit exceeded: {limit} {safety_class} invocations per {window}s for principal '{principal_id}'")
  • Use time.monotonic() internally for testability with injectable clock.

Acceptance Criteria

  • 61st READ invocation in 60s raises PolicyDenied with "Rate limit exceeded"
  • 11th WRITE invocation in 60s raises PolicyDenied
  • 3rd DESTRUCTIVE invocation in 60s raises PolicyDenied
  • Rates configurable per DefaultPolicyEngine constructor
  • "service" role gets 10x limits (600/100/20)
  • Rate limits are per (principal_id, capability_id) pair (not global)
  • Clock is injectable for deterministic testing (no time.sleep() in tests)
  • Window slides correctly (old entries expire)

Affected Files

  • src/agent_kernel/policy.py (RateLimiter class + evaluate() integration)
  • tests/test_policy.py (rate limit tests with mock clock)
  • docs/security.md (update gap list — rate limiting now implemented)

Metadata

Metadata

Assignees

Labels

complexity:averageModerate effort, some design neededphase:authRegistry, tokens, policypriority:highCore functionalitysecuritySecurity-related issue or hardeningsize:MMedium change, 50 to 200 linestype:featureNew functionality

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions