Merge pull request #782 from rager306/fix/safe-budget-mutation

fix: replace unsafe Arc mutation in PUT /api/budget with RwLock
This commit is contained in:
Jaber Jaber
2026-03-27 05:36:24 +03:00
committed by GitHub
2 changed files with 15 additions and 21 deletions
+14 -21
View File
@@ -40,6 +40,9 @@ pub struct AppState {
/// Avoids blocking the `/api/providers` endpoint on TCP timeouts to
/// unreachable local services. 60-second TTL.
pub provider_probe_cache: openfang_runtime::provider_health::ProbeCache,
/// Thread-safe mutable budget config. Updated via PUT /api/budget.
/// Initialized from `kernel.config.budget` at startup.
pub budget_config: Arc<tokio::sync::RwLock<openfang_types::config::BudgetConfig>>,
}
/// POST /api/agents — Spawn a new agent.
@@ -5266,10 +5269,8 @@ pub async fn usage_daily(State(state): State<Arc<AppState>>) -> impl IntoRespons
/// GET /api/budget — Current budget status (limits, spend, % used).
pub async fn budget_status(State(state): State<Arc<AppState>>) -> impl IntoResponse {
let status = state
.kernel
.metering
.budget_status(&state.kernel.config.budget);
let budget = state.budget_config.read().await;
let status = state.kernel.metering.budget_status(&budget);
Json(serde_json::to_value(&status).unwrap_or_default())
}
@@ -5278,34 +5279,26 @@ pub async fn update_budget(
State(state): State<Arc<AppState>>,
Json(body): Json<serde_json::Value>,
) -> impl IntoResponse {
// SAFETY: Budget config is updated in-place. Since KernelConfig is behind
// an Arc and we only have &self, we use ptr mutation (same pattern as OFP).
let config_ptr = &state.kernel.config as *const openfang_types::config::KernelConfig
as *mut openfang_types::config::KernelConfig;
// Apply updates
unsafe {
{
let mut budget = state.budget_config.write().await;
if let Some(v) = body["max_hourly_usd"].as_f64() {
(*config_ptr).budget.max_hourly_usd = v;
budget.max_hourly_usd = v;
}
if let Some(v) = body["max_daily_usd"].as_f64() {
(*config_ptr).budget.max_daily_usd = v;
budget.max_daily_usd = v;
}
if let Some(v) = body["max_monthly_usd"].as_f64() {
(*config_ptr).budget.max_monthly_usd = v;
budget.max_monthly_usd = v;
}
if let Some(v) = body["alert_threshold"].as_f64() {
(*config_ptr).budget.alert_threshold = v.clamp(0.0, 1.0);
budget.alert_threshold = v.clamp(0.0, 1.0);
}
if let Some(v) = body["default_max_llm_tokens_per_hour"].as_u64() {
(*config_ptr).budget.default_max_llm_tokens_per_hour = v;
budget.default_max_llm_tokens_per_hour = v;
}
}
let status = state
.kernel
.metering
.budget_status(&state.kernel.config.budget);
let budget = state.budget_config.read().await;
let status = state.kernel.metering.budget_status(&budget);
Json(serde_json::to_value(&status).unwrap_or_default())
}
+1
View File
@@ -51,6 +51,7 @@ pub async fn build_router(
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
clawhub_cache: dashmap::DashMap::new(),
provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(),
budget_config: Arc::new(tokio::sync::RwLock::new(kernel.config.budget.clone())),
});
// CORS: allow localhost origins by default. If API key is set, the API