/* RFC-0291: best-effort per-IP fixed-window rate limiting for the Agent Gate. In-isolate LRU counter — per-isolate, resets on eviction. Platform-level WAF rules are a deferred phase (nonGoal for v1). Fail-open on limiter internals (a limiter bug must not take the action tier down). Do not persist counters — in-memory only, short-lived. Do not implement exact cross-isolate limiting — best-effort by design. Do not move limit numbers here — they live in capability YAML only. RFC-0291: initial rate limiter. */ export interface RateLimitResult { allowed: boolean; retryAfterSeconds: number; } export interface RateLimiter { check(key: string): RateLimitResult; } /** * Create a fixed-window rate limiter. Each key gets its own bucket that resets * after `windowSeconds`. When a bucket exceeds `maxPerWindow`, subsequent * requests in the same window are denied with `retryAfterSeconds` indicating * how long until the window resets. * * Buckets are evicted lazily — expired entries are replaced on next access. * The limiter is intentionally simple: no LRU eviction, no cross-isolate sync. */ export function createFixedWindowLimiter(windowSeconds: number, maxPerWindow: number): RateLimiter { const buckets = new Map(); return { check(key: string): RateLimitResult { const now = Date.now(); let bucket = buckets.get(key); if (!bucket || now >= bucket.expiresAt) { bucket = { count: 0, expiresAt: now + windowSeconds * 1000 }; buckets.set(key, bucket); } bucket.count++; if (bucket.count > maxPerWindow) { const retryAfterSeconds = Math.max(1, Math.ceil((bucket.expiresAt - now) / 1000)); return { allowed: false, retryAfterSeconds }; } return { allowed: true, retryAfterSeconds: 0 }; }, }; }