refactor: comprehensive audit — fix bugs, harden security, deduplicate providers, add CI/Docker
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

Phase 1: Fix compilation (config_path Option<PathBuf>, streaming test, stale test cleanup)
Phase 2: Fix critical bugs (remove block_on deadlocks in 4 providers, fix broken SQL query builder)
Phase 3: Security hardening (session manager, real auth, token masking, Gemini key to header, password policy)
Phase 4: Implement stubs (real provider test, /proc health metrics, client/provider/backup endpoints, has_images)
Phase 5: Code quality (shared provider helpers, explicit re-exports, all Clippy warnings fixed, unwrap removal, 6 unused deps removed, dashboard split into 7 sub-modules)
Phase 6: Infrastructure (GitHub Actions CI, multi-stage Dockerfile, rustfmt.toml, clippy.toml, script fixes)
This commit is contained in:
2026-03-02 00:35:45 -05:00
parent ba643dd2b0
commit 2cdc49d7f2
42 changed files with 2800 additions and 2747 deletions

View File

@@ -5,12 +5,12 @@
//! 2. Provider circuit breaking to handle API failures
//! 3. Global rate limiting for overall system protection
use std::sync::Arc;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use tracing::{info, warn};
use anyhow::Result;
/// Rate limiter configuration
#[derive(Debug, Clone)]
@@ -26,8 +26,8 @@ pub struct RateLimiterConfig {
impl Default for RateLimiterConfig {
fn default() -> Self {
Self {
requests_per_minute: 60, // 1 request per second per client
burst_size: 10, // Allow bursts of up to 10 requests
requests_per_minute: 60, // 1 request per second per client
burst_size: 10, // Allow bursts of up to 10 requests
global_requests_per_minute: 600, // 10 requests per second globally
}
}
@@ -36,9 +36,9 @@ impl Default for RateLimiterConfig {
/// Circuit breaker state
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CircuitState {
Closed, // Normal operation
Open, // Circuit is open, requests fail fast
HalfOpen, // Testing if service has recovered
Closed, // Normal operation
Open, // Circuit is open, requests fail fast
HalfOpen, // Testing if service has recovered
}
/// Circuit breaker configuration
@@ -57,10 +57,10 @@ pub struct CircuitBreakerConfig {
impl Default for CircuitBreakerConfig {
fn default() -> Self {
Self {
failure_threshold: 5, // 5 failures
failure_window_secs: 60, // within 60 seconds
reset_timeout_secs: 30, // wait 30 seconds before half-open
success_threshold: 3, // 3 successes to close circuit
failure_threshold: 5, // 5 failures
failure_window_secs: 60, // within 60 seconds
reset_timeout_secs: 30, // wait 30 seconds before half-open
success_threshold: 3, // 3 successes to close circuit
}
}
}
@@ -88,14 +88,14 @@ impl TokenBucket {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill).as_secs_f64();
let new_tokens = elapsed * self.refill_rate;
self.tokens = (self.tokens + new_tokens).min(self.capacity);
self.last_refill = now;
}
fn try_acquire(&mut self, tokens: f64) -> bool {
self.refill();
if self.tokens >= tokens {
self.tokens -= tokens;
true
@@ -175,18 +175,18 @@ impl ProviderCircuitBreaker {
/// Record a failed request
pub fn record_failure(&mut self) {
let now = std::time::Instant::now();
// Check if failure window has expired
if let Some(last_failure) = self.last_failure_time {
if now.duration_since(last_failure).as_secs() > self.config.failure_window_secs {
// Reset failure count if window expired
self.failure_count = 0;
}
if let Some(last_failure) = self.last_failure_time
&& now.duration_since(last_failure).as_secs() > self.config.failure_window_secs
{
// Reset failure count if window expired
self.failure_count = 0;
}
self.failure_count += 1;
self.last_failure_time = Some(now);
if self.failure_count >= self.config.failure_threshold && self.state == CircuitState::Closed {
self.state = CircuitState::Open;
self.last_state_change = now;
@@ -220,7 +220,7 @@ impl RateLimitManager {
pub fn new(config: RateLimiterConfig, circuit_config: CircuitBreakerConfig) -> Self {
// Convert requests per minute to tokens per second
let global_refill_rate = config.global_requests_per_minute as f64 / 60.0;
Self {
client_buckets: Arc::new(RwLock::new(HashMap::new())),
global_bucket: Arc::new(RwLock::new(TokenBucket::new(
@@ -243,18 +243,16 @@ impl RateLimitManager {
return Ok(false);
}
}
// Check client-specific rate limit
let mut buckets = self.client_buckets.write().await;
let bucket = buckets
.entry(client_id.to_string())
.or_insert_with(|| {
TokenBucket::new(
self.config.burst_size as f64,
self.config.requests_per_minute as f64 / 60.0,
)
});
let bucket = buckets.entry(client_id.to_string()).or_insert_with(|| {
TokenBucket::new(
self.config.burst_size as f64,
self.config.requests_per_minute as f64 / 60.0,
)
});
Ok(bucket.try_acquire(1.0))
}
@@ -264,7 +262,7 @@ impl RateLimitManager {
let breaker = breakers
.entry(provider_name.to_string())
.or_insert_with(|| ProviderCircuitBreaker::new(self.circuit_config.clone()));
Ok(breaker.allow_request())
}
@@ -282,7 +280,7 @@ impl RateLimitManager {
let breaker = breakers
.entry(provider_name.to_string())
.or_insert_with(|| ProviderCircuitBreaker::new(self.circuit_config.clone()));
breaker.record_failure();
}
@@ -299,14 +297,13 @@ impl RateLimitManager {
/// Axum middleware for rate limiting
pub mod middleware {
use super::*;
use crate::errors::AppError;
use crate::state::AppState;
use axum::{
extract::{Request, State},
middleware::Next,
response::Response,
};
use crate::errors::AppError;
use crate::state::AppState;
/// Rate limiting middleware
pub async fn rate_limit_middleware(
@@ -319,41 +316,35 @@ pub mod middleware {
// Check rate limits
if !state.rate_limit_manager.check_client_request(&client_id).await? {
return Err(AppError::RateLimitError(
"Rate limit exceeded".to_string()
));
return Err(AppError::RateLimitError("Rate limit exceeded".to_string()));
}
Ok(next.run(request).await)
}
/// Extract client ID from request (helper function)
fn extract_client_id_from_request(request: &Request) -> String {
// Try to extract from Authorization header
if let Some(auth_header) = request.headers().get("Authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if auth_str.starts_with("Bearer ") {
let token = &auth_str[7..];
// Use token hash as client ID (same logic as auth module)
return format!("client_{}", &token[..8.min(token.len())]);
}
}
if let Some(auth_header) = request.headers().get("Authorization")
&& let Ok(auth_str) = auth_header.to_str()
&& let Some(token) = auth_str.strip_prefix("Bearer ")
{
// Use token hash as client ID (same logic as auth module)
return format!("client_{}", &token[..8.min(token.len())]);
}
// Fallback to anonymous
"anonymous".to_string()
}
/// Circuit breaker middleware for provider requests
pub async fn circuit_breaker_middleware(
provider_name: &str,
state: &AppState,
) -> Result<(), AppError> {
pub async fn circuit_breaker_middleware(provider_name: &str, state: &AppState) -> Result<(), AppError> {
if !state.rate_limit_manager.check_provider_request(provider_name).await? {
return Err(AppError::ProviderError(
format!("Provider {} is currently unavailable (circuit breaker open)", provider_name)
));
return Err(AppError::ProviderError(format!(
"Provider {} is currently unavailable (circuit breaker open)",
provider_name
)));
}
Ok(())
}
}
}