Skip to content

feat: add per-namespace agent rate limiting - #532

Open
ashike24 wants to merge 12 commits into
GenAI-Security-Project:mainfrom
ashike24:feat/week5-6-agent-rate-limiting
Open

feat: add per-namespace agent rate limiting#532
ashike24 wants to merge 12 commits into
GenAI-Security-Project:mainfrom
ashike24:feat/week5-6-agent-rate-limiting

Conversation

@ashike24

@ashike24 ashike24 commented Jun 25, 2026

Copy link
Copy Markdown

Summary

Adds per-namespace rate limiting to all agent-triggering endpoints in the FinBot vendor portal, protecting the shared LLM quota from exhaustion and ensuring fair access across namespaces.

Motivation

Without rate limiting, a single namespace generating a burst of requests could consume the entire available LLM quota, degrading the experience for every other user of the platform. This change caps the number of agent-invoking requests per namespace within a fixed time window.

Changes

finbot/config.py

  • Added AGENT_RATE_LIMIT_MAX (default: 10) and AGENT_RATE_LIMIT_WINDOW_SECONDS (default: 60) to the Settings class with full env var override support.

finbot/core/ratelimit/limiter.py (new)

  • Fixed-window Redis counter keyed on finbot:ratelimit:{namespace}:agent.
  • Reuses the existing event_bus.redis async client — no second Redis connection.
  • Raises HTTP 429 with a descriptive message including current count, max, and TTL remaining.
  • Fails open on Redis errors to avoid blocking all users due to infrastructure issues.

finbot/core/ratelimit/__init__.py (new)

  • Package marker.

finbot/apps/vendor/routes/api.py

  • Added Depends(check_agent_rate_limit) to 5 agent-triggering routes: POST /vendors/register, POST /vendors/{vendor_id}/request-review, POST /invoices, POST /invoices/{invoice_id}/reprocess, and POST /chat.

Test Results

10 integration tests added in tests/integration/test_rate_limiting.py, all passing. No regressions in the existing suite (382 passed, 6 pre-existing failures unchanged).

@ashike24 ashike24 changed the title feat: add per-namespace agent rate limiting (Week 5-6) feat: add per-namespace agent rate limiting Jun 26, 2026
ashike24 added 2 commits July 4, 2026 23:36
- New finbot/mcp_server/ package with submit_tool_call and
  get_scoring_results tools, wrapping VendorChatAssistant and
  UserChallengeProgressRepository respectively
- Auth via existing SessionManager (session_id -> SessionContext),
  no new auth mechanism needed
- Runs as separate Docker service (mcp_server) on port 8100,
  sharing sqlite_data volume + Redis with the main app
- Fixed pre-existing .env bug: DATABASE_URL pointed outside the
  mounted sqlite_data volume, causing data loss on container
  rebuild for both services
- Verified end-to-end: get_scoring_results fully working;
  submit_tool_call reaches the LLM call correctly, blocked only
  by a missing OPENAI_API_KEY in local dev env (pre-existing gap,
  unrelated to this change)
- start_challenge_session from original proposal wording dropped:
  no such concept exists in the codebase; sessions already exist
  via login and challenges progress implicitly
try:
# Fix 2: Explicitly guard Redis initialization
redis = getattr(event_bus, "redis", None)
if redis is None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fail-open rate limiting

If Redis is unavailable or not initialized, the middleware allows every request through, effectively disabling rate limiting. For LLM/agent endpoints, this creates a potential abuse vector during Redis outages.

Consider failing closed (HTTP 503/429) or using an in-memory fallback rate limiter instead of bypassing rate limiting completely.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The limiter now fails closed - if Redis is unavailable or uninitialized, the request is blocked with HTTP 503 rather than allowed through.

Comment thread finbot/core/ratelimit/limiter.py Outdated

# On the first request in a window, set the expiry
if count == 1:
await redis.expire(key, window_seconds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you check this -
Race condition between INCR and EXPIRE

INCR and EXPIRE are executed as separate Redis commands. If the process crashes or is interrupted after INCR succeeds but before EXPIRE executes, the key is created without a TTL. Since subsequent requests will observe count > 1, the expiration is never set, leaving a permanent rate-limit key.

Consider making these operations atomic using a Redis transaction/pipeline

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. INCR and EXPIRE are now executed atomically using a Redis pipeline with transaction=True, eliminating the race condition where a crash between the two commands could leave a permanent key with no TTL.

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.

2 participants