From 7dc3748799d3073aea5a89a909da899ea34d085f Mon Sep 17 00:00:00 2001 From: FerrousLogic Date: Fri, 5 Jun 2026 14:30:24 -0700 Subject: [PATCH 1/2] security: fix sandbox bypass, env race, and unsafe SAFETY docs - tool_media_describe/transcribe now route through workspace sandbox - API keys moved from process env to RwLock in AppState - Add SAFETY comment to libc::kill in kernel.rs --- crates/openfang-api/src/routes.rs | 155 +++++++++++++------ crates/openfang-api/src/server.rs | 20 +++ crates/openfang-kernel/src/kernel.rs | 5 + crates/openfang-runtime/src/model_catalog.rs | 64 ++++++++ crates/openfang-runtime/src/tool_runner.rs | 18 ++- 5 files changed, 207 insertions(+), 55 deletions(-) diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index cebb1f599a..ef2c3b51b0 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -43,6 +43,11 @@ pub struct AppState { /// Thread-safe mutable budget config. Updated via PUT /api/budget. /// Initialized from `kernel.config.budget` at startup. pub budget_config: Arc>, + /// Runtime API key store. Replaces `std::env::set_var`/`remove_var` (unsound in + /// multithreaded contexts). Keys written here are checked by `detect_auth_with_keys` + /// and presence checks throughout the API layer. Uses `std::sync::RwLock` (not + /// tokio's) so synchronous helpers like `build_field_json` can read without awaiting. + pub api_keys: Arc>>, } /// POST /api/agents — Spawn a new agent. @@ -2392,15 +2397,19 @@ fn is_channel_configured(config: &openfang_types::config::ChannelsConfig, name: } } -/// Build a JSON field descriptor, checking env var presence but never exposing secrets. +/// Build a JSON field descriptor, checking key presence but never exposing secrets. /// For non-secret fields, includes the actual config value from `config_values` if available. fn build_field_json( f: &ChannelField, config_values: Option<&serde_json::Value>, + api_keys: &HashMap, ) -> serde_json::Value { let has_value = f .env_var - .map(|ev| std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false)) + .map(|ev| { + api_keys.contains_key(ev) + || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) + }) .unwrap_or(false); let mut field = serde_json::json!({ "key": f.key, @@ -2651,6 +2660,7 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo // Read the live channels config (updated on every hot-reload) instead of the // stale boot-time kernel.config, so newly configured channels show correctly. let live_channels = state.channels_config.read().await; + let api_keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); let mut channels = Vec::new(); let mut configured_count = 0u32; @@ -2667,7 +2677,10 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo .filter(|f| f.required && f.env_var.is_some()) .all(|f| { f.env_var - .map(|ev| std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false)) + .map(|ev| { + api_keys.contains_key(ev) + || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) + }) .unwrap_or(true) }); @@ -2675,7 +2688,7 @@ pub async fn list_channels(State(state): State>) -> impl IntoRespo let fields: Vec = meta .fields .iter() - .map(|f| build_field_json(f, config_vals.as_ref())) + .map(|f| build_field_json(f, config_vals.as_ref(), &api_keys)) .collect(); channels.push(serde_json::json!({ @@ -2751,10 +2764,13 @@ pub async fn configure_channel( Json(serde_json::json!({"error": format!("Failed to write secret: {e}")})), ); } - // SAFETY: We are the only writer; this is a single-threaded config operation - unsafe { - std::env::set_var(env_var, value); - } + // Store in runtime key map so detect_auth_with_keys and presence checks + // reflect the new key without calling set_var (unsound in async context). + state + .api_keys + .write() + .unwrap_or_else(|e| e.into_inner()) + .insert(env_var.to_string(), value.to_string()); // Also write the env var NAME to config.toml so the channel section // is not empty and the kernel knows which env var to read. config_fields.insert( @@ -2835,10 +2851,11 @@ pub async fn remove_channel( for field_def in meta.fields { if let Some(env_var) = field_def.env_var { let _ = remove_secret_env(&secrets_path, env_var); - // SAFETY: Single-threaded config operation - unsafe { - std::env::remove_var(env_var); - } + state + .api_keys + .write() + .unwrap_or_else(|e| e.into_inner()) + .remove(env_var); } } @@ -2881,6 +2898,7 @@ pub async fn remove_channel( /// (for Telegram). When provided, sends a real test message to verify the bot can /// post to that channel. pub async fn test_channel( + State(state): State>, Path(name): Path, raw_body: axum::body::Bytes, ) -> impl IntoResponse { @@ -2894,12 +2912,18 @@ pub async fn test_channel( } }; + // Clone the keys map immediately so no RwLockReadGuard (which is !Send) + // is held across any await point in this async function. + let api_keys: HashMap = + state.api_keys.read().unwrap_or_else(|e| e.into_inner()).clone(); // Check all required env vars are set let mut missing = Vec::new(); for field_def in meta.fields { if field_def.required { if let Some(env_var) = field_def.env_var { - if std::env::var(env_var).map(|v| v.is_empty()).unwrap_or(true) { + let has = api_keys.contains_key(env_var) + || std::env::var(env_var).map(|v| !v.is_empty()).unwrap_or(false); + if !has { missing.push(env_var); } } @@ -2929,7 +2953,7 @@ pub async fn test_channel( .map(|s| s.to_string()); if let Some(target_id) = target { - match send_channel_test_message(&name, &target_id).await { + match send_channel_test_message(&name, &target_id, &api_keys).await { Ok(()) => { return ( StatusCode::OK, @@ -2961,14 +2985,24 @@ pub async fn test_channel( } /// Send a real test message to a specific channel/chat on the given platform. -async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Result<(), String> { +async fn send_channel_test_message( + channel_name: &str, + target_id: &str, + api_keys: &HashMap, +) -> Result<(), String> { + let resolve = |var: &str| -> Result { + api_keys + .get(var) + .cloned() + .or_else(|| std::env::var(var).ok().filter(|v| !v.is_empty())) + .ok_or_else(|| format!("{var} not set")) + }; let client = reqwest::Client::new(); let test_msg = "OpenFang test message — your channel is connected!"; match channel_name { "discord" => { - let token = std::env::var("DISCORD_BOT_TOKEN") - .map_err(|_| "DISCORD_BOT_TOKEN not set".to_string())?; + let token = resolve("DISCORD_BOT_TOKEN")?; let url = format!("https://discord.com/api/v10/channels/{target_id}/messages"); let resp = client .post(&url) @@ -2983,8 +3017,7 @@ async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Resul } } "telegram" => { - let token = std::env::var("TELEGRAM_BOT_TOKEN") - .map_err(|_| "TELEGRAM_BOT_TOKEN not set".to_string())?; + let token = resolve("TELEGRAM_BOT_TOKEN")?; let url = format!("https://api.telegram.org/bot{token}/sendMessage"); let resp = client .post(&url) @@ -2998,8 +3031,7 @@ async fn send_channel_test_message(channel_name: &str, target_id: &str) -> Resul } } "slack" => { - let token = std::env::var("SLACK_BOT_TOKEN") - .map_err(|_| "SLACK_BOT_TOKEN not set".to_string())?; + let token = resolve("SLACK_BOT_TOKEN")?; let url = "https://slack.com/api/chat.postMessage"; let resp = client .post(url) @@ -7705,16 +7737,24 @@ pub async fn set_provider_key( ); } - // Set env var in current process so detect_auth picks it up - std::env::set_var(&env_var, &key); - - // Refresh auth detection + // Store in runtime key map so detect_auth_with_keys and presence checks + // reflect the new key without calling set_var (unsound in async context). state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .insert(env_var.clone(), key.clone()); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } // Auto-switch default provider if current default has no working key. // This fixes the common case where a user adds e.g. a Gemini key via dashboard @@ -7740,10 +7780,11 @@ pub async fn set_provider_key( let current_has_key = if current_key_env.is_empty() { false } else { - std::env::var(¤t_key_env) - .ok() - .filter(|v| !v.is_empty()) - .is_some() + state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .contains_key(¤t_key_env) }; let switched = if !current_has_key && current_provider != name { // Find a default model for the newly-keyed provider @@ -7877,16 +7918,23 @@ pub async fn delete_provider_key( ); } - // Remove from process environment - std::env::remove_var(&env_var); - - // Refresh auth detection + // Remove from runtime key map state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .remove(&env_var); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } ( StatusCode::OK, @@ -7927,7 +7975,13 @@ pub async fn test_provider( } }; - let api_key = std::env::var(&env_var).ok(); + let api_key = state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .get(&env_var) + .cloned() + .or_else(|| state.kernel.resolve_credential(&env_var)); // Only require API key for providers that need one (skip local providers like ollama/vllm/lmstudio) if key_required && api_key.is_none() && !env_var.is_empty() { return ( @@ -12061,16 +12115,23 @@ pub async fn copilot_oauth_poll( ); } - // Set in current process - std::env::set_var("GITHUB_TOKEN", access_token.as_str()); - - // Refresh auth detection + // Store in runtime key map (avoids set_var which is unsound in async context) state - .kernel - .model_catalog + .api_keys .write() .unwrap_or_else(|e| e.into_inner()) - .detect_auth(); + .insert("GITHUB_TOKEN".to_string(), access_token.to_string()); + + // Refresh auth detection + { + let keys = state.api_keys.read().unwrap_or_else(|e| e.into_inner()); + state + .kernel + .model_catalog + .write() + .unwrap_or_else(|e| e.into_inner()) + .detect_auth_with_keys(&keys); + } // Clean up flow state COPILOT_FLOWS.remove(&poll_id); diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index a1a2bc9c06..6dfb4a2c8d 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -8,6 +8,7 @@ use crate::webchat; use crate::ws; use axum::Router; use openfang_kernel::OpenFangKernel; +use std::collections::HashMap; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; @@ -41,6 +42,24 @@ pub async fn build_router( // Start channel bridges (Telegram, etc.) let bridge = channel_bridge::start_channel_bridge(kernel.clone()).await; + // Pre-populate the runtime API key map from the credential resolver so keys + // stored in vault or secrets.env are visible at startup without set_var. + let api_keys = { + let mut map = HashMap::new(); + let catalog = kernel + .model_catalog + .read() + .unwrap_or_else(|e| e.into_inner()); + for provider in catalog.list_providers() { + if !provider.api_key_env.is_empty() { + if let Some(val) = kernel.resolve_credential(&provider.api_key_env) { + map.insert(provider.api_key_env.clone(), val); + } + } + } + Arc::new(std::sync::RwLock::new(map)) + }; + let channels_config = kernel.config.channels.clone(); let state = Arc::new(AppState { kernel: kernel.clone(), @@ -52,6 +71,7 @@ pub async fn build_router( 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())), + api_keys, }); // Start WS cron broadcaster — subscribes to kernel event bus and pushes diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 8f59414c97..a6d7ec2dad 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -5238,6 +5238,11 @@ impl OpenFangKernel { // Best-effort kill — don't block shutdown on failure #[cfg(unix)] { + // SAFETY: `pid` was stored when this process spawned the WhatsApp + // gateway child; the Mutex guard is held so no concurrent mutation + // of the PID can occur. If the child has already exited the kernel + // will have reaped it (or it became a zombie), in which case + // `kill(pid, SIGTERM)` returns ESRCH and is a harmless no-op here. unsafe { libc::kill(pid as i32, libc::SIGTERM); } diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 4bf6fc7ec8..98588748ad 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -116,6 +116,70 @@ impl ModelCatalog { } } + /// Detect which providers have API keys configured, also checking a runtime key map. + /// + /// Checks the provided `runtime_keys` map first, then falls back to `std::env::var()`. + /// This avoids calling `std::env::set_var` (unsound in multithreaded contexts) while + /// still reflecting keys that were added at runtime via the API. + pub fn detect_auth_with_keys(&mut self, runtime_keys: &HashMap) { + for provider in &mut self.providers { + if provider.id == "claude-code" { + provider.auth_status = if crate::drivers::claude_code::claude_code_available() { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if provider.id == "qwen-code" { + provider.auth_status = if crate::drivers::qwen_code::qwen_code_available() { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if provider.id == "github-copilot" || provider.id == "copilot" { + let openfang_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .map(|h| std::path::PathBuf::from(h).join(".openfang")) + .unwrap_or_else(|_| std::path::PathBuf::from(".openfang")); + provider.auth_status = + if runtime_keys.contains_key("GITHUB_TOKEN") + || crate::drivers::copilot::copilot_auth_available(&openfang_dir) + { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + continue; + } + if !provider.key_required { + provider.auth_status = AuthStatus::NotRequired; + continue; + } + let has_key = runtime_keys.contains_key(&provider.api_key_env) + || std::env::var(&provider.api_key_env).is_ok(); + let has_fallback = match provider.id.as_str() { + "gemini" => { + runtime_keys.contains_key("GOOGLE_API_KEY") + || std::env::var("GOOGLE_API_KEY").is_ok() + } + "codex" => { + runtime_keys.contains_key("OPENAI_API_KEY") + || std::env::var("OPENAI_API_KEY").is_ok() + || read_codex_credential().is_some() + } + _ => false, + }; + provider.auth_status = if has_key || has_fallback { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; + } + } + /// List all models in the catalog. pub fn list_models(&self) -> &[ModelCatalogEntry] { &self.models diff --git a/crates/openfang-runtime/src/tool_runner.rs b/crates/openfang-runtime/src/tool_runner.rs index 16695616ee..d9a430c16e 100644 --- a/crates/openfang-runtime/src/tool_runner.rs +++ b/crates/openfang-runtime/src/tool_runner.rs @@ -328,8 +328,8 @@ pub async fn execute_tool( "image_analyze" => tool_image_analyze(input).await, // Media understanding tools - "media_describe" => tool_media_describe(input, media_engine).await, - "media_transcribe" => tool_media_transcribe(input, media_engine).await, + "media_describe" => tool_media_describe(input, media_engine, workspace_root).await, + "media_transcribe" => tool_media_transcribe(input, media_engine, workspace_root).await, // Image generation tool "image_generate" => tool_image_generate(input, workspace_root, media_engine).await, @@ -2993,19 +2993,20 @@ fn tool_system_time() -> String { async fn tool_media_describe( input: &serde_json::Value, media_engine: Option<&crate::media_understanding::MediaEngine>, + workspace_root: Option<&Path>, ) -> Result { use base64::Engine; let engine = media_engine.ok_or("Media engine not available. Check media configuration.")?; let path = input["path"].as_str().ok_or("Missing 'path' parameter")?; - let _ = validate_path(path)?; + let resolved = resolve_file_path(path, workspace_root)?; // Read image file - let data = tokio::fs::read(path) + let data = tokio::fs::read(&resolved) .await .map_err(|e| format!("Failed to read image file: {e}"))?; // Detect MIME type from extension - let ext = std::path::Path::new(path) + let ext = resolved .extension() .and_then(|e| e.to_str()) .unwrap_or("") @@ -3038,19 +3039,20 @@ async fn tool_media_describe( async fn tool_media_transcribe( input: &serde_json::Value, media_engine: Option<&crate::media_understanding::MediaEngine>, + workspace_root: Option<&Path>, ) -> Result { use base64::Engine; let engine = media_engine.ok_or("Media engine not available. Check media configuration.")?; let path = input["path"].as_str().ok_or("Missing 'path' parameter")?; - let _ = validate_path(path)?; + let resolved = resolve_file_path(path, workspace_root)?; // Read audio file - let data = tokio::fs::read(path) + let data = tokio::fs::read(&resolved) .await .map_err(|e| format!("Failed to read audio file: {e}"))?; // Detect MIME type from extension - let ext = std::path::Path::new(path) + let ext = resolved .extension() .and_then(|e| e.to_str()) .unwrap_or("") From d1d1c8f931c2431c0413432b8a7d1fe78c5331bc Mon Sep 17 00:00:00 2001 From: FerrousLogic Date: Fri, 5 Jun 2026 20:58:08 -0700 Subject: [PATCH 2/2] fix: rustfmt and missing api_keys in test AppState initializers --- crates/openfang-api/src/routes.rs | 14 +++++++++----- crates/openfang-api/tests/api_integration_test.rs | 3 +++ .../openfang-api/tests/daemon_lifecycle_test.rs | 3 +++ crates/openfang-api/tests/load_test.rs | 2 ++ .../openfang-api/tests/skill_config_api_test.rs | 2 ++ crates/openfang-runtime/src/model_catalog.rs | 15 +++++++-------- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index ef2c3b51b0..97893033b5 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -2407,8 +2407,7 @@ fn build_field_json( let has_value = f .env_var .map(|ev| { - api_keys.contains_key(ev) - || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) + api_keys.contains_key(ev) || std::env::var(ev).map(|v| !v.is_empty()).unwrap_or(false) }) .unwrap_or(false); let mut field = serde_json::json!({ @@ -2914,15 +2913,20 @@ pub async fn test_channel( // Clone the keys map immediately so no RwLockReadGuard (which is !Send) // is held across any await point in this async function. - let api_keys: HashMap = - state.api_keys.read().unwrap_or_else(|e| e.into_inner()).clone(); + let api_keys: HashMap = state + .api_keys + .read() + .unwrap_or_else(|e| e.into_inner()) + .clone(); // Check all required env vars are set let mut missing = Vec::new(); for field_def in meta.fields { if field_def.required { if let Some(env_var) = field_def.env_var { let has = api_keys.contains_key(env_var) - || std::env::var(env_var).map(|v| !v.is_empty()).unwrap_or(false); + || std::env::var(env_var) + .map(|v| !v.is_empty()) + .unwrap_or(false); if !has { missing.push(env_var); } diff --git a/crates/openfang-api/tests/api_integration_test.rs b/crates/openfang-api/tests/api_integration_test.rs index d31a2ef651..0c95187554 100644 --- a/crates/openfang-api/tests/api_integration_test.rs +++ b/crates/openfang-api/tests/api_integration_test.rs @@ -13,6 +13,7 @@ use openfang_api::routes::{self, AppState}; use openfang_api::ws; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -80,6 +81,7 @@ async fn start_test_server_with_provider( clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() @@ -925,6 +927,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let api_key = state.kernel.config.api_key.trim().to_string(); diff --git a/crates/openfang-api/tests/daemon_lifecycle_test.rs b/crates/openfang-api/tests/daemon_lifecycle_test.rs index c62cba9b7a..744440442c 100644 --- a/crates/openfang-api/tests/daemon_lifecycle_test.rs +++ b/crates/openfang-api/tests/daemon_lifecycle_test.rs @@ -9,6 +9,7 @@ use openfang_api::routes::{self, AppState}; use openfang_api::server::{read_daemon_info, DaemonInfo}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -117,6 +118,7 @@ async fn test_full_daemon_lifecycle() { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() @@ -244,6 +246,7 @@ async fn test_server_immediate_responsiveness() { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-api/tests/load_test.rs b/crates/openfang-api/tests/load_test.rs index c0bfc9ae59..3005278788 100644 --- a/crates/openfang-api/tests/load_test.rs +++ b/crates/openfang-api/tests/load_test.rs @@ -10,6 +10,7 @@ use openfang_api::middleware; use openfang_api::routes::{self, AppState}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use tower_http::cors::CorsLayer; @@ -61,6 +62,7 @@ async fn start_test_server() -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-api/tests/skill_config_api_test.rs b/crates/openfang-api/tests/skill_config_api_test.rs index e8a1e60a72..9e0a72a6f5 100644 --- a/crates/openfang-api/tests/skill_config_api_test.rs +++ b/crates/openfang-api/tests/skill_config_api_test.rs @@ -13,6 +13,7 @@ use openfang_api::middleware; use openfang_api::routes::{self, AppState}; use openfang_kernel::OpenFangKernel; use openfang_types::config::{DefaultModelConfig, KernelConfig}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Instant; use tower_http::cors::CorsLayer; @@ -98,6 +99,7 @@ async fn start_test_server() -> TestServer { clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), budget_config: Arc::new(tokio::sync::RwLock::new(Default::default())), + api_keys: Arc::new(std::sync::RwLock::new(HashMap::new())), }); let app = Router::new() diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 98588748ad..63d0314bcf 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -144,14 +144,13 @@ impl ModelCatalog { .or_else(|_| std::env::var("USERPROFILE")) .map(|h| std::path::PathBuf::from(h).join(".openfang")) .unwrap_or_else(|_| std::path::PathBuf::from(".openfang")); - provider.auth_status = - if runtime_keys.contains_key("GITHUB_TOKEN") - || crate::drivers::copilot::copilot_auth_available(&openfang_dir) - { - AuthStatus::Configured - } else { - AuthStatus::Missing - }; + provider.auth_status = if runtime_keys.contains_key("GITHUB_TOKEN") + || crate::drivers::copilot::copilot_auth_available(&openfang_dir) + { + AuthStatus::Configured + } else { + AuthStatus::Missing + }; continue; } if !provider.key_required {