diff --git a/.gitignore b/.gitignore index 78b7238..81c6b72 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ Thumbs.db *.swp *.swo *~ + +# Generated desktop schemas +crates/openfang-desktop/gen/ diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index 7d95a54..16017b5 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -1092,7 +1092,9 @@ pub async fn start_channel_bridge_with_config( // WhatsApp — supports Cloud API mode (access token) or Web/QR mode (gateway URL) if let Some(ref wa_config) = config.whatsapp { let cloud_token = read_token(&wa_config.access_token_env, "WhatsApp"); - let gateway_url = std::env::var(&wa_config.gateway_url_env).ok().filter(|u| !u.is_empty()); + let gateway_url = std::env::var(&wa_config.gateway_url_env) + .ok() + .filter(|u| !u.is_empty()); if cloud_token.is_some() || gateway_url.is_some() { let token = cloud_token.unwrap_or_default(); diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index ed36d68..2b95938 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -5960,8 +5960,7 @@ pub async fn set_provider_url( } // Probe reachability at the new URL - let probe = - openfang_runtime::provider_health::probe_provider(&name, &base_url).await; + let probe = openfang_runtime::provider_health::probe_provider(&name, &base_url).await; ( StatusCode::OK, diff --git a/crates/openfang-api/src/ws.rs b/crates/openfang-api/src/ws.rs index f24c70b..b3cdd77 100644 --- a/crates/openfang-api/src/ws.rs +++ b/crates/openfang-api/src/ws.rs @@ -1112,7 +1112,8 @@ fn classify_streaming_error(err: &openfang_kernel::error::KernelError) -> String "Model unavailable. Use /model to see options.".to_string() } llm_errors::LlmErrorCategory::Format => { - "LLM request failed. Check your API key and model configuration in Settings.".to_string() + "LLM request failed. Check your API key and model configuration in Settings." + .to_string() } _ => classified.sanitized_message, } diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index 08ee3d7..d5f9501 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -3169,11 +3169,11 @@

Runtime Configuration

@@ -3261,8 +3261,8 @@

A2A External Agents

this.budgetLoading = true; try { let [b, a] = await Promise.all([ - fetch('/api/budget').then(r => r.json()), - fetch('/api/budget/agents').then(r => r.json()) + OpenFangAPI.get('/api/budget'), + OpenFangAPI.get('/api/budget/agents') ]); this.budgetData = b; this.agentRanking = a.agents || []; @@ -3285,7 +3285,7 @@

A2A External Agents

if (+this.editMonthly !== this.budgetData.monthly_limit) body.max_monthly_usd = +this.editMonthly; let alertVal = (+this.editAlert) / 100; if (Math.abs(alertVal - this.budgetData.alert_threshold) > 0.001) body.alert_threshold = alertVal; - await fetch('/api/budget', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }); + await OpenFangAPI.put('/api/budget', body); this.editMode = false; await this.loadBudget(); } catch(e) { alert('Failed to save: ' + e); } diff --git a/crates/openfang-api/static/js/api.js b/crates/openfang-api/static/js/api.js index 5a82805..040d4b5 100644 --- a/crates/openfang-api/static/js/api.js +++ b/crates/openfang-api/static/js/api.js @@ -118,7 +118,7 @@ var OpenFangToast = (function() { // ── Friendly Error Messages ── function friendlyError(status, serverMsg) { if (status === 0 || !status) return 'Cannot reach daemon — is openfang running?'; - if (status === 401) return 'Not authorized — check your API key'; + if (status === 401) return 'Not authorized — include Bearer API key (or open dashboard with ?token=...)'; if (status === 403) return 'Permission denied'; if (status === 404) return serverMsg || 'Resource not found'; if (status === 429) return 'Rate limited — slow down and try again'; @@ -134,12 +134,36 @@ var OpenFangAPI = (function() { var WS_BASE = BASE.replace(/^http/, 'ws'); var _authToken = ''; + function initAuthToken() { + try { + var params = new URLSearchParams(window.location.search || ''); + var fromQuery = params.get('token') || params.get('api_key') || ''; + if (fromQuery) { + _authToken = fromQuery; + sessionStorage.setItem('openfang_auth_token', fromQuery); + return; + } + var fromSession = sessionStorage.getItem('openfang_auth_token') || ''; + if (fromSession) _authToken = fromSession; + } catch(e) { + // Ignore parse/storage errors. + } + } + + initAuthToken(); + // Connection state tracking var _connectionState = 'connected'; var _reconnectAttempt = 0; var _connectionListeners = []; - function setAuthToken(token) { _authToken = token; } + function setAuthToken(token) { + _authToken = token || ''; + try { + if (_authToken) sessionStorage.setItem('openfang_auth_token', _authToken); + else sessionStorage.removeItem('openfang_auth_token'); + } catch(e) {} + } function headers() { var h = { 'Content-Type': 'application/json' }; diff --git a/crates/openfang-channels/src/whatsapp.rs b/crates/openfang-channels/src/whatsapp.rs index 82ad584..9f45fcd 100644 --- a/crates/openfang-channels/src/whatsapp.rs +++ b/crates/openfang-channels/src/whatsapp.rs @@ -222,11 +222,9 @@ impl ChannelAdapter for WhatsAppAdapter { if let Some(ref gw) = self.gateway_url { let text = match &content { ChannelContent::Text(t) => t.clone(), - ChannelContent::Image { caption, .. } => { - caption - .clone() - .unwrap_or_else(|| "(Image — not supported in Web mode)".to_string()) - } + ChannelContent::Image { caption, .. } => caption + .clone() + .unwrap_or_else(|| "(Image — not supported in Web mode)".to_string()), ChannelContent::File { filename, .. } => { format!("(File: {filename} — not supported in Web mode)") } diff --git a/crates/openfang-cli/src/bundled_agents.rs b/crates/openfang-cli/src/bundled_agents.rs index d1e036c..0194859 100644 --- a/crates/openfang-cli/src/bundled_agents.rs +++ b/crates/openfang-cli/src/bundled_agents.rs @@ -7,34 +7,112 @@ /// Returns all bundled agent templates as `(name, toml_content)` pairs. pub fn bundled_agents() -> Vec<(&'static str, &'static str)> { vec![ - ("analyst", include_str!("../../../agents/analyst/agent.toml")), - ("architect", include_str!("../../../agents/architect/agent.toml")), - ("assistant", include_str!("../../../agents/assistant/agent.toml")), + ( + "analyst", + include_str!("../../../agents/analyst/agent.toml"), + ), + ( + "architect", + include_str!("../../../agents/architect/agent.toml"), + ), + ( + "assistant", + include_str!("../../../agents/assistant/agent.toml"), + ), ("coder", include_str!("../../../agents/coder/agent.toml")), - ("code-reviewer", include_str!("../../../agents/code-reviewer/agent.toml")), - ("customer-support", include_str!("../../../agents/customer-support/agent.toml")), - ("data-scientist", include_str!("../../../agents/data-scientist/agent.toml")), - ("debugger", include_str!("../../../agents/debugger/agent.toml")), - ("devops-lead", include_str!("../../../agents/devops-lead/agent.toml")), - ("doc-writer", include_str!("../../../agents/doc-writer/agent.toml")), - ("email-assistant", include_str!("../../../agents/email-assistant/agent.toml")), - ("health-tracker", include_str!("../../../agents/health-tracker/agent.toml")), - ("hello-world", include_str!("../../../agents/hello-world/agent.toml")), - ("home-automation", include_str!("../../../agents/home-automation/agent.toml")), - ("legal-assistant", include_str!("../../../agents/legal-assistant/agent.toml")), - ("meeting-assistant", include_str!("../../../agents/meeting-assistant/agent.toml")), + ( + "code-reviewer", + include_str!("../../../agents/code-reviewer/agent.toml"), + ), + ( + "customer-support", + include_str!("../../../agents/customer-support/agent.toml"), + ), + ( + "data-scientist", + include_str!("../../../agents/data-scientist/agent.toml"), + ), + ( + "debugger", + include_str!("../../../agents/debugger/agent.toml"), + ), + ( + "devops-lead", + include_str!("../../../agents/devops-lead/agent.toml"), + ), + ( + "doc-writer", + include_str!("../../../agents/doc-writer/agent.toml"), + ), + ( + "email-assistant", + include_str!("../../../agents/email-assistant/agent.toml"), + ), + ( + "health-tracker", + include_str!("../../../agents/health-tracker/agent.toml"), + ), + ( + "hello-world", + include_str!("../../../agents/hello-world/agent.toml"), + ), + ( + "home-automation", + include_str!("../../../agents/home-automation/agent.toml"), + ), + ( + "legal-assistant", + include_str!("../../../agents/legal-assistant/agent.toml"), + ), + ( + "meeting-assistant", + include_str!("../../../agents/meeting-assistant/agent.toml"), + ), ("ops", include_str!("../../../agents/ops/agent.toml")), - ("orchestrator", include_str!("../../../agents/orchestrator/agent.toml")), - ("personal-finance", include_str!("../../../agents/personal-finance/agent.toml")), - ("planner", include_str!("../../../agents/planner/agent.toml")), - ("recruiter", include_str!("../../../agents/recruiter/agent.toml")), - ("researcher", include_str!("../../../agents/researcher/agent.toml")), - ("sales-assistant", include_str!("../../../agents/sales-assistant/agent.toml")), - ("security-auditor", include_str!("../../../agents/security-auditor/agent.toml")), - ("social-media", include_str!("../../../agents/social-media/agent.toml")), - ("test-engineer", include_str!("../../../agents/test-engineer/agent.toml")), - ("translator", include_str!("../../../agents/translator/agent.toml")), - ("travel-planner", include_str!("../../../agents/travel-planner/agent.toml")), + ( + "orchestrator", + include_str!("../../../agents/orchestrator/agent.toml"), + ), + ( + "personal-finance", + include_str!("../../../agents/personal-finance/agent.toml"), + ), + ( + "planner", + include_str!("../../../agents/planner/agent.toml"), + ), + ( + "recruiter", + include_str!("../../../agents/recruiter/agent.toml"), + ), + ( + "researcher", + include_str!("../../../agents/researcher/agent.toml"), + ), + ( + "sales-assistant", + include_str!("../../../agents/sales-assistant/agent.toml"), + ), + ( + "security-auditor", + include_str!("../../../agents/security-auditor/agent.toml"), + ), + ( + "social-media", + include_str!("../../../agents/social-media/agent.toml"), + ), + ( + "test-engineer", + include_str!("../../../agents/test-engineer/agent.toml"), + ), + ( + "translator", + include_str!("../../../agents/translator/agent.toml"), + ), + ( + "travel-planner", + include_str!("../../../agents/travel-planner/agent.toml"), + ), ("tutor", include_str!("../../../agents/tutor/agent.toml")), ("writer", include_str!("../../../agents/writer/agent.toml")), ] diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index 5d908ff..00602e8 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -968,10 +968,36 @@ pub(crate) fn find_daemon() -> Option { /// Build an HTTP client for daemon calls. pub(crate) fn daemon_client() -> reqwest::blocking::Client { - reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .build() - .expect("Failed to build HTTP client") + let mut builder = + reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(120)); + + if let Some(api_key) = daemon_api_key() { + let mut headers = reqwest::header::HeaderMap::new(); + let auth_value = format!("Bearer {api_key}"); + if let Ok(mut hv) = reqwest::header::HeaderValue::from_str(&auth_value) { + hv.set_sensitive(true); + headers.insert(reqwest::header::AUTHORIZATION, hv); + builder = builder.default_headers(headers); + } + } + + builder.build().expect("Failed to build HTTP client") +} + +fn daemon_api_key() -> Option { + let config_path = dirs::home_dir()?.join(".openfang").join("config.toml"); + let content = std::fs::read_to_string(config_path).ok()?; + parse_api_key_from_config_toml(&content) +} + +fn parse_api_key_from_config_toml(content: &str) -> Option { + let table: toml::Value = toml::from_str(content).ok()?; + let api_key = table.get("api_key")?.as_str()?.trim(); + if api_key.is_empty() { + None + } else { + Some(api_key.to_string()) + } } /// Helper: send a request to the daemon and parse the JSON body. @@ -5530,6 +5556,7 @@ fn cmd_reset(confirm: bool) { #[cfg(test)] mod tests { + use super::parse_api_key_from_config_toml; // --- Doctor command unit tests --- @@ -5652,4 +5679,63 @@ args = ["-y", "@modelcontextprotocol/server-github"] ]; assert_eq!(events.len(), 4); } + + #[test] + fn test_parse_api_key_from_config_toml_present() { + let config = r#" +api_listen = "127.0.0.1:4200" +api_key = "test-secret" + +[default_model] +provider = "groq" +model = "llama-3.3-70b-versatile" +api_key_env = "GROQ_API_KEY" +"#; + let parsed = parse_api_key_from_config_toml(config); + assert_eq!(parsed.as_deref(), Some("test-secret")); + } + + #[test] + fn test_parse_api_key_from_config_toml_empty_or_missing() { + let with_empty = r#" +api_listen = "127.0.0.1:4200" +api_key = "" + +[default_model] +provider = "groq" +model = "llama-3.3-70b-versatile" +api_key_env = "GROQ_API_KEY" +"#; + let missing = r#" +api_listen = "127.0.0.1:4200" + +[default_model] +provider = "groq" +model = "llama-3.3-70b-versatile" +api_key_env = "GROQ_API_KEY" +"#; + assert_eq!(parse_api_key_from_config_toml(with_empty), None); + assert_eq!(parse_api_key_from_config_toml(missing), None); + } + + #[test] + fn test_parse_api_key_from_config_toml_trims_value() { + let config = r#" +api_listen = "127.0.0.1:4200" +api_key = " test-secret " +"#; + assert_eq!( + parse_api_key_from_config_toml(config).as_deref(), + Some("test-secret") + ); + } + + #[test] + fn test_parse_api_key_from_config_toml_invalid_toml() { + let invalid = r#" +api_listen = "127.0.0.1:4200" +api_key = "test-secret +"#; + assert_eq!(parse_api_key_from_config_toml(invalid), None); + } } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index c825be4..7289625 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -2223,15 +2223,11 @@ impl OpenFangKernel { /// Switch an agent's model. pub fn set_agent_model(&self, agent_id: AgentId, model: &str) -> KernelResult<()> { // Resolve provider from model catalog so switching models also switches provider - let resolved_provider = self - .model_catalog - .read() - .ok() - .and_then(|catalog| { - catalog - .find_model(model) - .map(|entry| entry.provider.clone()) - }); + let resolved_provider = self.model_catalog.read().ok().and_then(|catalog| { + catalog + .find_model(model) + .map(|entry| entry.provider.clone()) + }); if let Some(provider) = resolved_provider { self.registry @@ -4072,7 +4068,8 @@ impl OpenFangKernel { tool_names.join(", ") )); } - summary.push_str("MCP tools are prefixed with mcp_{server}_ and work like regular tools.\n"); + summary + .push_str("MCP tools are prefixed with mcp_{server}_ and work like regular tools.\n"); // Add filesystem-specific guidance when a filesystem MCP server is connected let has_filesystem = servers.keys().any(|s| s.contains("filesystem")); if has_filesystem { diff --git a/crates/openfang-kernel/src/whatsapp_gateway.rs b/crates/openfang-kernel/src/whatsapp_gateway.rs index a4214a7..17b3aa0 100644 --- a/crates/openfang-kernel/src/whatsapp_gateway.rs +++ b/crates/openfang-kernel/src/whatsapp_gateway.rs @@ -10,10 +10,8 @@ use std::sync::Arc; use tracing::{info, warn}; /// Gateway source files embedded at compile time. -const GATEWAY_INDEX_JS: &str = - include_str!("../../../packages/whatsapp-gateway/index.js"); -const GATEWAY_PACKAGE_JSON: &str = - include_str!("../../../packages/whatsapp-gateway/package.json"); +const GATEWAY_INDEX_JS: &str = include_str!("../../../packages/whatsapp-gateway/index.js"); +const GATEWAY_PACKAGE_JSON: &str = include_str!("../../../packages/whatsapp-gateway/package.json"); /// Default port for the WhatsApp Web gateway. const DEFAULT_GATEWAY_PORT: u16 = 3009; @@ -69,8 +67,8 @@ async fn ensure_gateway_installed() -> Result { let package_path = dir.join("package.json"); // Write files only if content changed (avoids unnecessary npm install) - let index_changed = - write_if_changed(&index_path, GATEWAY_INDEX_JS).map_err(|e| format!("Write index.js: {e}"))?; + let index_changed = write_if_changed(&index_path, GATEWAY_INDEX_JS) + .map_err(|e| format!("Write index.js: {e}"))?; let package_changed = write_if_changed(&package_path, GATEWAY_PACKAGE_JSON) .map_err(|e| format!("Write package.json: {e}"))?; @@ -164,7 +162,10 @@ pub async fn start_whatsapp_gateway(kernel: &Arc) .to_string(); // Auto-set the env var so the rest of the system finds the gateway - std::env::set_var("WHATSAPP_WEB_GATEWAY_URL", format!("http://127.0.0.1:{port}")); + std::env::set_var( + "WHATSAPP_WEB_GATEWAY_URL", + format!("http://127.0.0.1:{port}"), + ); info!("WHATSAPP_WEB_GATEWAY_URL set to http://127.0.0.1:{port}"); // Spawn with crash monitoring @@ -247,9 +248,7 @@ pub async fn start_whatsapp_gateway(kernel: &Arc) restarts += 1; if restarts >= MAX_RESTARTS { - warn!( - "WhatsApp gateway exceeded max restarts ({MAX_RESTARTS}), giving up" - ); + warn!("WhatsApp gateway exceeded max restarts ({MAX_RESTARTS}), giving up"); return; } diff --git a/crates/openfang-runtime/src/compactor.rs b/crates/openfang-runtime/src/compactor.rs index f06d668..7569f65 100644 --- a/crates/openfang-runtime/src/compactor.rs +++ b/crates/openfang-runtime/src/compactor.rs @@ -343,7 +343,11 @@ fn build_conversation_text(messages: &[Message], config: &CompactionConfig) -> S if oversized { let limit = config.max_chunk_chars / 4; let truncated = if s.len() > limit { - format!("{}...[truncated from {} chars]", safe_truncate_str(s, limit), s.len()) + format!( + "{}...[truncated from {} chars]", + safe_truncate_str(s, limit), + s.len() + ) } else { s.clone() }; diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index dfbebf4..fb84448 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -15,9 +15,9 @@ use openfang_types::model_catalog::{ AI21_BASE_URL, ANTHROPIC_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, - OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, - ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, + OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, + SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, ZHIPU_BASE_URL, + ZHIPU_CODING_BASE_URL, }; use std::sync::Arc; diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index b0ef9d6..a6505ad 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -2632,10 +2632,7 @@ mod tests { #[test] fn test_resolve_alias() { let catalog = ModelCatalog::new(); - assert_eq!( - catalog.resolve_alias("sonnet"), - Some("claude-sonnet-4-6") - ); + assert_eq!(catalog.resolve_alias("sonnet"), Some("claude-sonnet-4-6")); assert_eq!( catalog.resolve_alias("haiku"), Some("claude-haiku-4-5-20251001") diff --git a/crates/openfang-runtime/src/str_utils.rs b/crates/openfang-runtime/src/str_utils.rs index beb13a8..00ba72c 100644 --- a/crates/openfang-runtime/src/str_utils.rs +++ b/crates/openfang-runtime/src/str_utils.rs @@ -44,7 +44,7 @@ mod tests { fn multibyte_chinese() { // Each Chinese character is 3 bytes in UTF-8 let s = "\u{4f60}\u{597d}\u{4e16}\u{754c}"; // "hello world" in Chinese, 12 bytes - // Truncating at 7 bytes should not split the 3rd char (bytes 6..9) + // Truncating at 7 bytes should not split the 3rd char (bytes 6..9) let t = safe_truncate_str(s, 7); assert_eq!(t, "\u{4f60}\u{597d}"); // 6 bytes, 2 chars assert!(t.len() <= 7);