arbiter_lifecycle/
state.rs1use arbiter_audit::AuditSink;
2use arbiter_identity::{AnyRegistry, InMemoryRegistry};
3use arbiter_metrics::ArbiterMetrics;
4use arbiter_policy::PolicyConfig;
5use arbiter_session::{AnySessionStore, SessionStore};
6use std::collections::{HashMap, VecDeque};
7use std::sync::Arc;
8use std::sync::Mutex;
9use std::time::{Duration, Instant};
10use tokio::sync::watch;
11use uuid::Uuid;
12
13use crate::token::TokenConfig;
14
15pub struct AdminRateLimiter {
21 clients: Mutex<HashMap<String, VecDeque<Instant>>>,
23 max_requests: u64,
25 window_duration: Duration,
27}
28
29impl AdminRateLimiter {
30 pub fn new(max_requests: u64, window_duration: Duration) -> Self {
32 Self {
33 clients: Mutex::new(HashMap::new()),
34 max_requests,
35 window_duration,
36 }
37 }
38
39 pub fn check_rate_limit(&self, client_key: &str) -> bool {
44 let now = Instant::now();
45 let mut clients = self.clients.lock().unwrap_or_else(|e| e.into_inner());
46 let window = clients.entry(client_key.to_string()).or_default();
47
48 while let Some(&front) = window.front() {
50 if now.duration_since(front) > self.window_duration {
51 window.pop_front();
52 } else {
53 break;
54 }
55 }
56
57 if (window.len() as u64) >= self.max_requests {
58 false
59 } else {
60 window.push_back(now);
61 true
62 }
63 }
64
65 pub fn check_rate_limit_global(&self) -> bool {
67 self.check_rate_limit("__global__")
68 }
69}
70
71#[derive(Clone)]
73pub struct AppState {
74 pub registry: Arc<AnyRegistry>,
75 pub admin_api_key: String,
76 pub token_config: TokenConfig,
77 pub session_store: AnySessionStore,
78 pub policy_config: Arc<watch::Sender<Arc<Option<PolicyConfig>>>>,
79 pub audit_sink: Option<Arc<AuditSink>>,
81 pub policy_file_path: Option<String>,
83 pub warning_threshold_pct: f64,
85 pub default_rate_limit_window_secs: u64,
87 pub metrics: Arc<ArbiterMetrics>,
89 pub max_concurrent_sessions_per_agent: Option<u64>,
92 pub admin_rate_limiter: Arc<AdminRateLimiter>,
95}
96
97const DEFAULT_ADMIN_MAX_REQUESTS_PER_MINUTE: u64 = 60;
99
100pub const MIN_ADMIN_API_KEY_LEN: usize = 16;
103
104impl AppState {
105 pub fn new(admin_api_key: String) -> Self {
107 if admin_api_key.len() < MIN_ADMIN_API_KEY_LEN {
108 tracing::error!(
109 length = admin_api_key.len(),
110 minimum = MIN_ADMIN_API_KEY_LEN,
111 "admin API key is shorter than minimum recommended length; \
112 the gateway will start but authentication is weakened"
113 );
114 }
115 Self {
116 registry: Arc::new(AnyRegistry::InMemory(InMemoryRegistry::new())),
117 admin_api_key,
118 token_config: TokenConfig::default(),
119 session_store: AnySessionStore::InMemory(SessionStore::new()),
120 policy_config: Arc::new(watch::channel(Arc::new(None)).0),
121 audit_sink: None,
122 policy_file_path: None,
123 warning_threshold_pct: 20.0,
124 default_rate_limit_window_secs: 60,
125 metrics: Arc::new(ArbiterMetrics::new().expect("metrics registry init")),
126 max_concurrent_sessions_per_agent: Some(10),
127 admin_rate_limiter: Arc::new(AdminRateLimiter::new(
128 DEFAULT_ADMIN_MAX_REQUESTS_PER_MINUTE,
129 Duration::from_secs(60),
130 )),
131 }
132 }
133
134 pub fn with_token_config(admin_api_key: String, token_config: TokenConfig) -> Self {
136 Self {
137 registry: Arc::new(AnyRegistry::InMemory(InMemoryRegistry::new())),
138 admin_api_key,
139 token_config,
140 session_store: AnySessionStore::InMemory(SessionStore::new()),
141 policy_config: Arc::new(watch::channel(Arc::new(None)).0),
142 audit_sink: None,
143 policy_file_path: None,
144 warning_threshold_pct: 20.0,
145 default_rate_limit_window_secs: 60,
146 metrics: Arc::new(ArbiterMetrics::new().expect("metrics registry init")),
147 max_concurrent_sessions_per_agent: Some(10),
148 admin_rate_limiter: Arc::new(AdminRateLimiter::new(
149 DEFAULT_ADMIN_MAX_REQUESTS_PER_MINUTE,
150 Duration::from_secs(60),
151 )),
152 }
153 }
154
155 pub fn with_admin_rate_limit(mut self, max_requests_per_minute: u64) -> Self {
157 self.admin_rate_limiter = Arc::new(AdminRateLimiter::new(
158 max_requests_per_minute,
159 Duration::from_secs(60),
160 ));
161 self
162 }
163
164 pub fn admin_audit_log(&self, operation: &str, agent_id: Option<Uuid>, detail: &str) {
169 match agent_id {
170 Some(id) => {
171 tracing::info!(
172 operation = operation,
173 agent_id = %id,
174 detail = detail,
175 "ADMIN_AUDIT: admin API operation"
176 );
177 }
178 None => {
179 tracing::info!(
180 operation = operation,
181 detail = detail,
182 "ADMIN_AUDIT: admin API operation"
183 );
184 }
185 }
186 }
187}