Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ Thumbs.db
*.swp
*.swo
*~

# Generated desktop schemas
crates/openfang-desktop/gen/
4 changes: 3 additions & 1 deletion crates/openfang-api/src/channel_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion crates/openfang-api/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
12 changes: 6 additions & 6 deletions crates/openfang-api/static/index_body.html
Original file line number Diff line number Diff line change
Expand Up @@ -3169,11 +3169,11 @@ <h4>Runtime Configuration</h4>
<!-- Network tab -->
<div x-show="tab === 'network'" x-data="{
netStatus: null, a2aAgents: [], a2aDiscoverUrl: '', a2aDiscovering: false,
async loadNetStatus() { try { this.netStatus = await (await fetch('/api/network/status')).json(); } catch(e) {} },
async loadA2aAgents() { try { let r = await (await fetch('/api/a2a/agents')).json(); this.a2aAgents = r.agents || []; } catch(e) {} },
async loadNetStatus() { try { this.netStatus = await OpenFangAPI.get('/api/network/status'); } catch(e) {} },
async loadA2aAgents() { try { let r = await OpenFangAPI.get('/api/a2a/agents'); this.a2aAgents = r.agents || []; } catch(e) {} },
async discoverA2a() {
if (!this.a2aDiscoverUrl) return; this.a2aDiscovering = true;
try { await fetch('/api/a2a/discover', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.a2aDiscoverUrl})}); this.a2aDiscoverUrl=''; await this.loadA2aAgents(); } catch(e) {}
try { await OpenFangAPI.post('/api/a2a/discover', {url:this.a2aDiscoverUrl}); this.a2aDiscoverUrl=''; await this.loadA2aAgents(); } catch(e) {}
this.a2aDiscovering = false;
}
}" x-init="loadNetStatus(); loadA2aAgents()">
Expand Down Expand Up @@ -3261,8 +3261,8 @@ <h4 style="margin-bottom:8px">A2A External Agents</h4>
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 || [];
Expand All @@ -3285,7 +3285,7 @@ <h4 style="margin-bottom:8px">A2A External Agents</h4>
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); }
Expand Down
28 changes: 26 additions & 2 deletions crates/openfang-api/static/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' };
Expand Down
8 changes: 3 additions & 5 deletions crates/openfang-channels/src/whatsapp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand Down
130 changes: 104 additions & 26 deletions crates/openfang-cli/src/bundled_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
]
Expand Down
94 changes: 90 additions & 4 deletions crates/openfang-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,10 +968,36 @@ pub(crate) fn find_daemon() -> Option<String> {

/// 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<String> {
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<String> {
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.
Expand Down Expand Up @@ -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 ---

Expand Down Expand Up @@ -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);
}
}
Loading