Skip to content

feat: add sliding-window rate limiting to DefaultPolicyEngine#64

Merged
dgenio merged 8 commits intomainfrom
feat/rate-limiting-policy-engine
Mar 14, 2026
Merged

feat: add sliding-window rate limiting to DefaultPolicyEngine#64
dgenio merged 8 commits intomainfrom
feat/rate-limiting-policy-engine

Conversation

@dgenio
Copy link
Owner

@dgenio dgenio commented Mar 14, 2026

What changed

Adds per-(principal_id, capability_id) sliding-window rate limiting to DefaultPolicyEngine, closing the #1 security gap identified in docs/security.md.

Files touched

  • src/agent_kernel/policy.py — Added RateLimiter class (sliding-window, injectable clock) and _RateEntry dataclass. Added __init__ to DefaultPolicyEngine with optional rate_limits and clock params. Rate check integrated at end of evaluate(), after all role/sensitivity/row-cap checks pass, before returning PolicyDecision.
  • tests/test_policy.py — 8 new tests covering all acceptance criteria with mock clock (no time.sleep()).
  • docs/security.md — Replaced "no rate limiting" disclaimer with feature documentation.
  • CHANGELOG.md — Added entry under [Unreleased].

Why

Issue #39: A compromised or runaway agent can invoke capabilities at unlimited rate, exhausting API quotas, running up costs, or performing DoS against backends. Rate limiting was explicitly called out as the #1 security gap.

Design decisions

  • Placement: Rate check runs after all existing policy checks pass (role, sensitivity, row-cap), before the final PolicyDecision return — exactly as specified in the issue.
  • Key: f"{principal_id}:{capability_id}" — scoped per principal-capability pair, not global.
  • Defaults: 60 READ / 10 WRITE / 2 DESTRUCTIVE invocations per 60s window.
  • Service role: 10× multiplier on limits for principals with "service" role.
  • No new dependencies: ~25 lines of straightforward Python; external libraries would add complexity without benefit.
  • Backward compatible: DefaultPolicyEngine() with no args works identically to before (all params optional with defaults).

How verified

Check Result
ruff format 36 files unchanged
ruff check All checks passed
mypy src/ 1 pre-existing error (httpx stubs), no new errors
pytest -q 225 passed in 0.33s (29 existing policy tests + 8 new)

Acceptance criteria coverage

Criterion Test
61st READ in 60s → PolicyDenied test_read_rate_limit_exceeded
11th WRITE in 60s → PolicyDenied test_write_rate_limit_exceeded
3rd DESTRUCTIVE in 60s → PolicyDenied test_destructive_rate_limit_exceeded
Configurable via constructor test_rate_limit_configurable
Service role gets 10× test_service_role_gets_10x_limit
Per (principal_id, capability_id) test_rate_limit_per_principal_capability_pair
Injectable clock, no time.sleep() All rate tests use _make_clock()
Window slides correctly test_rate_limit_window_slides + test_rate_limiter_window_expires

Risks / limitations

  • Rate-limit state is in-process only — resets on restart, not shared across workers. Documented in docs/security.md.
  • No thread safety (consistent with existing single-threaded async pattern).

Docs & agent instructions

  • docs/security.md: Updated — "no rate limiting" replaced with feature documentation.
  • CHANGELOG.md: Updated with entry under [Unreleased].
  • Agent instructions (AGENTS.md, .github/copilot-instructions.md, .claude/CLAUDE.md): Reviewed — no updates needed. Existing "adding a policy rule" guidance and security invariants already cover this feature.

Closes #39

Add RateLimiter class and integrate per-(principal_id, capability_id) rate
limiting into DefaultPolicyEngine.evaluate(). Defaults by safety class:
60 READ / 10 WRITE / 2 DESTRUCTIVE per 60s window. Service-role principals
get 10x limits. Clock is injectable for deterministic testing.

- policy.py: RateLimiter + DefaultPolicyEngine.__init__ with rate_limits/clock
- test_policy.py: 8 new tests covering all acceptance criteria (mock clock)
- docs/security.md: replace 'no rate limiting' with feature documentation
- CHANGELOG.md: add entry under [Unreleased]

Closes #39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds sliding-window rate limiting to DefaultPolicyEngine to mitigate runaway/compromised principals invoking capabilities at unbounded rates, aligning policy enforcement with the security threat model documented in docs/security.md.

Changes:

  • Introduces RateLimiter (injectable clock) and integrates per-(principal_id, capability_id) enforcement into DefaultPolicyEngine.evaluate().
  • Adds rate-limiting tests using a controllable clock (no time.sleep()).
  • Updates security documentation and changelog to reflect the new control.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 11 comments.

File Description
src/agent_kernel/policy.py Adds RateLimiter, default rate limit config, and enforcement in DefaultPolicyEngine.
tests/test_policy.py Adds unit tests for rate limiter behavior and engine enforcement scenarios.
docs/security.md Replaces “no rate limiting” disclaimer with rate limiting documentation and limitations.
CHANGELOG.md Notes the new policy rate-limiting feature under [Unreleased].

You can also share your feedback on Copilot code review. Take the survey.

@dgenio dgenio merged commit e9021c1 into main Mar 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate limiting in PolicyEngine

2 participants