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 @@
@@ -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);