Skip to content
Closed
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
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 @@ -1065,7 +1065,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
373 changes: 273 additions & 100 deletions crates/openfang-api/src/routes.rs

Large diffs are not rendered by default.

26 changes: 7 additions & 19 deletions crates/openfang-api/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,7 @@ pub async fn build_router(
)
.route(
"/api/hands/{hand_id}/settings",
axum::routing::get(routes::get_hand_settings)
.put(routes::update_hand_settings),
axum::routing::get(routes::get_hand_settings).put(routes::update_hand_settings),
)
.route(
"/api/hands/instances/{id}/pause",
Expand Down Expand Up @@ -412,14 +411,8 @@ pub async fn build_router(
"/api/comms/events/stream",
axum::routing::get(routes::comms_events_stream),
)
.route(
"/api/comms/send",
axum::routing::post(routes::comms_send),
)
.route(
"/api/comms/task",
axum::routing::post(routes::comms_task),
)
.route("/api/comms/send", axum::routing::post(routes::comms_send))
.route("/api/comms/task", axum::routing::post(routes::comms_task))
// Tools endpoint
.route("/api/tools", axum::routing::get(routes::list_tools))
// Config endpoints
Expand Down Expand Up @@ -464,8 +457,7 @@ pub async fn build_router(
)
.route(
"/api/budget/agents/{id}",
axum::routing::get(routes::agent_budget_status)
.put(routes::update_agent_budget),
axum::routing::get(routes::agent_budget_status).put(routes::update_agent_budget),
)
// Session endpoints
.route("/api/sessions", axum::routing::get(routes::list_sessions))
Expand Down Expand Up @@ -787,8 +779,7 @@ pub async fn run_daemon(
socket.set_nonblocking(true)?;
socket.bind(&addr.into())?;
socket.listen(1024)?;
let listener =
tokio::net::TcpListener::from_std(std::net::TcpListener::from(socket))?;
let listener = tokio::net::TcpListener::from_std(std::net::TcpListener::from(socket))?;

// Run server with graceful shutdown.
// SECURITY: `into_make_service_with_connect_info` injects the peer
Expand Down Expand Up @@ -919,11 +910,8 @@ fn is_daemon_responding(addr: &str) -> bool {
.or_else(|| addr.strip_prefix("https://"))
.unwrap_or(addr);
if let Ok(sock_addr) = addr_only.parse::<std::net::SocketAddr>() {
std::net::TcpStream::connect_timeout(
&sock_addr,
std::time::Duration::from_millis(500),
)
.is_ok()
std::net::TcpStream::connect_timeout(&sock_addr, std::time::Duration::from_millis(500))
.is_ok()
} else {
// Fallback: try connecting to hostname
std::net::TcpStream::connect(addr_only)
Expand Down
11 changes: 8 additions & 3 deletions crates/openfang-api/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,10 @@ async fn handle_command(
match state.kernel.set_agent_model(agent_id, args) {
Ok(()) => {
let msg = if let Some(entry) = state.kernel.registry.get(agent_id) {
format!("Model switched to: {} (provider: {})", entry.manifest.model.model, entry.manifest.model.provider)
format!(
"Model switched to: {} (provider: {})",
entry.manifest.model.model, entry.manifest.model.provider
)
} else {
format!("Model switched to: {args}")
};
Expand Down Expand Up @@ -1121,11 +1124,13 @@ fn classify_streaming_error(err: &openfang_kernel::error::KernelError) -> String
if inner.contains("localhost:11434") || inner.contains("ollama") {
"Model not found on Ollama. Run `ollama pull <model>` to download it, then try again. Use /model to see options.".to_string()
} else {
"Model unavailable. Use /model to see options or check your provider configuration.".to_string()
"Model unavailable. Use /model to see options or check your provider configuration."
.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
4 changes: 3 additions & 1 deletion crates/openfang-channels/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,9 @@ async fn dispatch_message(
}
GroupPolicy::MentionOnly => {
// Only allow messages where the bot was @mentioned or commands.
let was_mentioned = message.metadata.get("was_mentioned")
let was_mentioned = message
.metadata
.get("was_mentioned")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let is_command = matches!(&message.content, ChannelContent::Command { .. });
Expand Down
29 changes: 21 additions & 8 deletions crates/openfang-channels/src/discord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,13 @@ impl ChannelAdapter for DiscordAdapter {
}

"MESSAGE_CREATE" | "MESSAGE_UPDATE" => {
if let Some(msg) =
parse_discord_message(d, &bot_user_id, &allowed_guilds, &allowed_users)
.await
if let Some(msg) = parse_discord_message(
d,
&bot_user_id,
&allowed_guilds,
&allowed_users,
)
.await
{
debug!(
"Discord {event_name} from {}: {:?}",
Expand Down Expand Up @@ -512,8 +516,8 @@ async fn parse_discord_message(
.map(|arr| arr.iter().any(|m| m["id"].as_str() == Some(bid.as_str())))
.unwrap_or(false);
// Also check content for <@bot_id> or <@!bot_id> patterns
let mentioned_in_content =
content_text.contains(&format!("<@{bid}>")) || content_text.contains(&format!("<@!{bid}>"));
let mentioned_in_content = content_text.contains(&format!("<@{bid}>"))
|| content_text.contains(&format!("<@!{bid}>"));
mentioned_in_array || mentioned_in_content
} else {
false
Expand Down Expand Up @@ -736,7 +740,8 @@ mod tests {
});

// Not in allowed users
let msg = parse_discord_message(&d, &bot_id, &[], &["user111".into(), "user222".into()]).await;
let msg =
parse_discord_message(&d, &bot_id, &[], &["user111".into(), "user222".into()]).await;
assert!(msg.is_none());

// In allowed users
Expand Down Expand Up @@ -769,7 +774,10 @@ mod tests {

let msg = parse_discord_message(&d, &bot_id, &[], &[]).await.unwrap();
assert!(msg.is_group);
assert_eq!(msg.metadata.get("was_mentioned").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
msg.metadata.get("was_mentioned").and_then(|v| v.as_bool()),
Some(true)
);

// Message without mention in group
let d2 = serde_json::json!({
Expand Down Expand Up @@ -811,7 +819,12 @@ mod tests {

#[test]
fn test_discord_adapter_creation() {
let adapter = DiscordAdapter::new("test-token".to_string(), vec!["123".to_string(), "456".to_string()], vec![], 37376);
let adapter = DiscordAdapter::new(
"test-token".to_string(),
vec!["123".to_string(), "456".to_string()],
vec![],
37376,
);
assert_eq!(adapter.name(), "discord");
assert_eq!(adapter.channel_type(), ChannelType::Discord);
}
Expand Down
10 changes: 4 additions & 6 deletions crates/openfang-channels/src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ impl EmailAdapter {
async fn build_smtp_transport(
&self,
) -> Result<AsyncSmtpTransport<Tokio1Executor>, Box<dyn std::error::Error>> {
let creds =
Credentials::new(self.username.clone(), self.password.as_str().to_string());
let creds = Credentials::new(self.username.clone(), self.password.as_str().to_string());

let transport = if self.smtp_port == 465 {
// Implicit TLS (port 465)
Expand Down Expand Up @@ -200,8 +199,8 @@ fn fetch_unseen_emails(
.build()
.map_err(|e| format!("TLS connector error: {e}"))?;

let client = imap::connect((host, port), host, &tls)
.map_err(|e| format!("IMAP connect failed: {e}"))?;
let client =
imap::connect((host, port), host, &tls).map_err(|e| format!("IMAP connect failed: {e}"))?;

let mut session = client
.login(username, password)
Expand Down Expand Up @@ -362,8 +361,7 @@ impl ChannelAdapter for EmailAdapter {
}

// Extract target agent from subject brackets (stored in metadata for router)
let _target_agent =
EmailAdapter::extract_agent_from_subject(&subject);
let _target_agent = EmailAdapter::extract_agent_from_subject(&subject);
let clean_subject = EmailAdapter::strip_agent_tag(&subject);

// Build the message body: prepend subject context
Expand Down
12 changes: 11 additions & 1 deletion crates/openfang-channels/src/telegram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,17 @@ pub fn calculate_backoff(current: Duration) -> Duration {
/// Everything else (e.g. `<name>`, `<thinking>`) gets escaped to `&lt;...&gt;`.
fn sanitize_telegram_html(text: &str) -> String {
const ALLOWED: &[&str] = &[
"b", "i", "u", "s", "em", "strong", "a", "code", "pre", "blockquote", "tg-spoiler",
"b",
"i",
"u",
"s",
"em",
"strong",
"a",
"code",
"pre",
"blockquote",
"tg-spoiler",
"tg-emoji",
];

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
Loading