Skip to content

Commit 7ce51e6

Browse files
anchapinAuto Merge Bot
andauthored
feat: Add rate limiting and quota management for agent operations (#489)
* feat: Add rate limiting and quota management for agent operations Implements per-agent and per-user rate limiting with configurable quotas for API calls, VM spawning, and approval requests. Features: - Token bucket algorithm for smooth rate limiting - Per-agent and per-user quota tracking - Configurable limits for different operation types - Admin dashboard support for monitoring and adjusting quotas - Admin user exemptions from rate limits Components: - RateLimitConfig: Configuration with environment variable support - Quota: Token bucket implementation with refill logic - QuotaStore: In-memory storage for quota data - RateLimitManager: Central manager for rate limit checks - DashboardData: Admin dashboard support Tests: 43 unit tests covering all components Closes: luminaguard-548 * fix: Replace or_insert_with(Vec::new) with or_default() Clippy on macOS was failing with: error: use of `or_insert_with` to construct default value --> src/rate_limit/dashboard.rs:115:26 This fixes the clippy lint error that was blocking PR #489. * fix: Apply cargo fmt to rate_limit module Fix formatting issues in rate_limit module files: - config.rs - dashboard.rs - manager.rs - mod.rs - quota.rs - store.rs This fixes the cargo fmt check failure. * feat: Optimize VM startup time to sub-100ms baseline (#490) * feat: Optimize VM startup time to sub-100ms baseline Implements profiling and optimization for VM startup time with target <100ms. Features: - StartupProfiler for measuring VM startup performance - Phase-by-phase breakdown (config validation, resource prep, process spawn, etc.) - PerformanceTracker for aggregating startup profiles - Optimization recommendations based on profiling data - Global performance tracker for system-wide metrics Optimization Strategies Documented: 1. Pre-warmed Snapshot Pool: Maintain ready-to-use VM snapshots 2. Lazy Initialization: Defer non-critical initialization 3. Parallel Resource Setup: Concurrent kernel/rootfs loading 4. Memory Pre-allocation: Pre-allocate VM memory regions 5. Socket Optimization: Use faster Unix socket patterns Performance Targets: - Cold boot: <100ms (current: ~110ms) - Snapshot load: <20ms - Pool acquire: <5ms Tests: 7 unit tests covering all components Closes: luminaguard-2xw * bd sync: 2026-02-19 14:09:32 * fix: Fix formatting issues in startup_optimizer module - Move startup_optimizer module to correct alphabetical order in mod.rs - Fix line length formatting in startup_optimizer.rs - Add trailing newline at end of file This fixes the cargo fmt check failure that was blocking PR #490. --------- Co-authored-by: Auto Merge Bot <auto-merge@ironclaw.app> * feat: Add rate limiting and quota management for agent operations Implements per-agent and per-user rate limiting with configurable quotas for API calls, VM spawning, and approval requests. Features: - Token bucket algorithm for smooth rate limiting - Per-agent and per-user quota tracking - Configurable limits for different operation types - Admin dashboard support for monitoring and adjusting quotas - Admin user exemptions from rate limits Components: - RateLimitConfig: Configuration with environment variable support - Quota: Token bucket implementation with refill logic - QuotaStore: In-memory storage for quota data - RateLimitManager: Central manager for rate limit checks - DashboardData: Admin dashboard support Tests: 43 unit tests covering all components Closes: luminaguard-548 * fix: Replace or_insert_with(Vec::new) with or_default() Clippy on macOS was failing with: error: use of `or_insert_with` to construct default value --> src/rate_limit/dashboard.rs:115:26 This fixes the clippy lint error that was blocking PR #489. * fix: Apply cargo fmt to rate_limit module Fix formatting issues in rate_limit module files: - config.rs - dashboard.rs - manager.rs - mod.rs - quota.rs - store.rs This fixes the cargo fmt check failure. --------- Co-authored-by: Auto Merge Bot <auto-merge@ironclaw.app>
1 parent 52e68d0 commit 7ce51e6

8 files changed

Lines changed: 2033 additions & 91 deletions

File tree

.beads/issues.jsonl

Lines changed: 91 additions & 91 deletions
Large diffs are not rendered by default.

orchestrator/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ pub mod mcp;
99
pub mod mcp_command;
1010
pub mod metrics;
1111
pub mod metrics_server;
12+
pub mod rate_limit;
1213
pub mod vm;
1314
pub mod webhooks;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//! Rate Limit Configuration
2+
//!
3+
//! Configuration for rate limiting and quota management.
4+
5+
use serde::{Deserialize, Serialize};
6+
use std::time::Duration;
7+
8+
/// Default rate limits
9+
pub const DEFAULT_API_RATE_LIMIT: u32 = 100; // requests per minute
10+
pub const DEFAULT_VM_SPAWN_LIMIT: u32 = 10; // VMs per hour
11+
pub const DEFAULT_APPROVAL_LIMIT: u32 = 50; // approvals per hour
12+
pub const DEFAULT_BURST_SIZE: u32 = 20; // burst allowance
13+
14+
/// Rate limit configuration
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct RateLimitConfig {
17+
/// Enable rate limiting
18+
pub enabled: bool,
19+
20+
/// Default API rate limit (requests per minute)
21+
pub default_api_limit: u32,
22+
23+
/// Default VM spawn limit (VMs per hour)
24+
pub default_vm_spawn_limit: u32,
25+
26+
/// Default approval request limit (requests per hour)
27+
pub default_approval_limit: u32,
28+
29+
/// Default burst size (allows temporary spikes)
30+
pub default_burst_size: u32,
31+
32+
/// Token refill interval in seconds
33+
pub refill_interval_secs: u64,
34+
35+
/// Quota reset interval in hours
36+
pub quota_reset_interval_hours: u64,
37+
38+
/// Per-user quota overrides
39+
#[serde(default)]
40+
pub user_overrides: std::collections::HashMap<String, UserQuotaOverride>,
41+
42+
/// Per-agent quota overrides
43+
#[serde(default)]
44+
pub agent_overrides: std::collections::HashMap<String, AgentQuotaOverride>,
45+
}
46+
47+
impl Default for RateLimitConfig {
48+
fn default() -> Self {
49+
Self {
50+
enabled: true,
51+
default_api_limit: DEFAULT_API_RATE_LIMIT,
52+
default_vm_spawn_limit: DEFAULT_VM_SPAWN_LIMIT,
53+
default_approval_limit: DEFAULT_APPROVAL_LIMIT,
54+
default_burst_size: DEFAULT_BURST_SIZE,
55+
refill_interval_secs: 60,
56+
quota_reset_interval_hours: 24,
57+
user_overrides: std::collections::HashMap::new(),
58+
agent_overrides: std::collections::HashMap::new(),
59+
}
60+
}
61+
}
62+
63+
impl RateLimitConfig {
64+
/// Create a new rate limit configuration
65+
pub fn new() -> Self {
66+
Self::default()
67+
}
68+
69+
/// Load configuration from environment variables
70+
pub fn from_env() -> Self {
71+
let mut config = Self::default();
72+
73+
if let Ok(val) = std::env::var("LUMINAGUARD_RATE_LIMIT_ENABLED") {
74+
config.enabled = val.parse().unwrap_or(true);
75+
}
76+
77+
if let Ok(val) = std::env::var("LUMINAGUARD_API_RATE_LIMIT") {
78+
if let Ok(limit) = val.parse() {
79+
config.default_api_limit = limit;
80+
}
81+
}
82+
83+
if let Ok(val) = std::env::var("LUMINAGUARD_VM_SPAWN_LIMIT") {
84+
if let Ok(limit) = val.parse() {
85+
config.default_vm_spawn_limit = limit;
86+
}
87+
}
88+
89+
if let Ok(val) = std::env::var("LUMINAGUARD_APPROVAL_LIMIT") {
90+
if let Ok(limit) = val.parse() {
91+
config.default_approval_limit = limit;
92+
}
93+
}
94+
95+
if let Ok(val) = std::env::var("LUMINAGUARD_BURST_SIZE") {
96+
if let Ok(size) = val.parse() {
97+
config.default_burst_size = size;
98+
}
99+
}
100+
101+
config
102+
}
103+
104+
/// Get refill duration
105+
pub fn refill_duration(&self) -> Duration {
106+
Duration::from_secs(self.refill_interval_secs)
107+
}
108+
109+
/// Get quota reset duration
110+
pub fn quota_reset_duration(&self) -> Duration {
111+
Duration::from_secs(self.quota_reset_interval_hours * 3600)
112+
}
113+
114+
/// Disable rate limiting (for testing)
115+
pub fn disabled() -> Self {
116+
Self {
117+
enabled: false,
118+
..Self::default()
119+
}
120+
}
121+
}
122+
123+
/// Per-user quota override
124+
#[derive(Debug, Clone, Serialize, Deserialize)]
125+
pub struct UserQuotaOverride {
126+
/// User ID
127+
pub user_id: String,
128+
129+
/// API rate limit override
130+
pub api_limit: Option<u32>,
131+
132+
/// VM spawn limit override
133+
pub vm_spawn_limit: Option<u32>,
134+
135+
/// Approval limit override
136+
pub approval_limit: Option<u32>,
137+
138+
/// Burst size override
139+
pub burst_size: Option<u32>,
140+
141+
/// Is this user an admin (exempt from limits)
142+
pub is_admin: bool,
143+
}
144+
145+
/// Per-agent quota override
146+
#[derive(Debug, Clone, Serialize, Deserialize)]
147+
pub struct AgentQuotaOverride {
148+
/// Agent ID
149+
pub agent_id: String,
150+
151+
/// API rate limit override
152+
pub api_limit: Option<u32>,
153+
154+
/// VM spawn limit override
155+
pub vm_spawn_limit: Option<u32>,
156+
157+
/// Approval limit override
158+
pub approval_limit: Option<u32>,
159+
160+
/// Burst size override
161+
pub burst_size: Option<u32>,
162+
}
163+
164+
#[cfg(test)]
165+
mod tests {
166+
use super::*;
167+
168+
#[test]
169+
fn test_default_config() {
170+
let config = RateLimitConfig::default();
171+
assert!(config.enabled);
172+
assert_eq!(config.default_api_limit, DEFAULT_API_RATE_LIMIT);
173+
assert_eq!(config.default_vm_spawn_limit, DEFAULT_VM_SPAWN_LIMIT);
174+
}
175+
176+
#[test]
177+
fn test_disabled_config() {
178+
let config = RateLimitConfig::disabled();
179+
assert!(!config.enabled);
180+
}
181+
182+
#[test]
183+
fn test_refill_duration() {
184+
let config = RateLimitConfig::default();
185+
assert_eq!(config.refill_duration(), Duration::from_secs(60));
186+
}
187+
188+
#[test]
189+
fn test_quota_reset_duration() {
190+
let config = RateLimitConfig::default();
191+
assert_eq!(
192+
config.quota_reset_duration(),
193+
Duration::from_secs(24 * 3600)
194+
);
195+
}
196+
197+
#[test]
198+
fn test_config_serialization() {
199+
let config = RateLimitConfig::default();
200+
let json = serde_json::to_string(&config).unwrap();
201+
let parsed: RateLimitConfig = serde_json::from_str(&json).unwrap();
202+
assert_eq!(config.default_api_limit, parsed.default_api_limit);
203+
}
204+
}

0 commit comments

Comments
 (0)