From eba9198827c11c218e105a0aa71ed769eceeec97 Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Sun, 8 Mar 2026 22:29:54 +0300 Subject: [PATCH] community fixes --- .github/workflows/release.yml | 4 +- Cargo.toml | 2 +- README.md | 14 +++-- SECURITY.md | 4 +- crates/openfang-api/src/middleware.rs | 1 + crates/openfang-api/src/routes.rs | 49 ++++++++++++++-- crates/openfang-api/src/types.rs | 9 ++- crates/openfang-api/static/index_body.html | 3 +- .../openfang-api/static/js/pages/settings.js | 7 ++- crates/openfang-cli/src/main.rs | 20 +++++-- crates/openfang-cli/src/mcp.rs | 20 ++++--- .../src/tui/screens/init_wizard.rs | 7 +++ .../openfang-extensions/integrations/aws.toml | 2 +- .../integrations/azure-mcp.toml | 2 +- .../integrations/bitbucket.toml | 2 +- .../integrations/brave-search.toml | 2 +- .../integrations/discord-mcp.toml | 2 +- .../integrations/dropbox.toml | 2 +- .../integrations/elasticsearch.toml | 2 +- .../integrations/exa-search.toml | 2 +- .../integrations/gcp-mcp.toml | 2 +- .../integrations/github.toml | 2 +- .../integrations/gitlab.toml | 2 +- .../integrations/gmail.toml | 2 +- .../integrations/google-calendar.toml | 2 +- .../integrations/google-drive.toml | 2 +- .../integrations/jira.toml | 2 +- .../integrations/linear.toml | 2 +- .../integrations/mongodb.toml | 2 +- .../integrations/notion.toml | 2 +- .../integrations/postgresql.toml | 2 +- .../integrations/redis.toml | 2 +- .../integrations/sentry.toml | 2 +- .../integrations/slack.toml | 2 +- .../integrations/sqlite-mcp.toml | 2 +- .../integrations/teams-mcp.toml | 2 +- .../integrations/todoist.toml | 2 +- crates/openfang-kernel/src/config.rs | 19 +++++++ crates/openfang-kernel/src/kernel.rs | 4 +- crates/openfang-runtime/src/agent_loop.rs | 34 +++++++++++ crates/openfang-skills/src/clawhub.rs | 56 +++++++++++++------ docs/security.md | 2 + scripts/install.sh | 18 +++++- 43 files changed, 245 insertions(+), 78 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdd08b40..471f37a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,7 +183,9 @@ jobs: run: cargo build --release --target ${{ matrix.target }} --bin openfang - name: Ad-hoc codesign CLI binary (macOS) if: runner.os == 'macOS' - run: codesign --force --sign - target/${{ matrix.target }}/release/openfang + run: | + xattr -cr target/${{ matrix.target }}/release/openfang || true + codesign --force --sign - target/${{ matrix.target }}/release/openfang - name: Package (Unix) if: matrix.archive == 'tar.gz' run: | diff --git a/Cargo.toml b/Cargo.toml index 872a1f1d..9b7f5da8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.3.30" +version = "0.3.31" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RightNow-AI/openfang" diff --git a/README.md b/README.md index 746b8427..bfe8b3d1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

Rust MIT - v0.1.0 + v0.3.30 Tests Clippy Buy Me A Coffee @@ -27,9 +27,9 @@ --- -> **v0.1.0 — First Release (February 2026)** +> **v0.3.30 — Security Hardening Release (March 2026)** > -> OpenFang is feature-complete but this is the first public release. You may encounter instability, rough edges, or breaking changes between minor versions. We ship fast and fix fast. Pin to a specific commit for production use until v1.0. [Report issues here.](https://github.com/RightNow-AI/openfang/issues) +> OpenFang is feature-complete but still pre-1.0. You may encounter rough edges or breaking changes between minor versions. We ship fast and fix fast. Pin to a specific commit for production use until v1.0. [Report issues here.](https://github.com/RightNow-AI/openfang/issues) --- @@ -371,7 +371,7 @@ cargo fmt --all -- --check ## Stability Notice -OpenFang v0.1.0 is the first public release. The architecture is solid, the test suite is comprehensive, and the security model is comprehensive. That said: +OpenFang v0.3.30 is pre-1.0. The architecture is solid, the test suite is comprehensive, and the security model is comprehensive. That said: - **Breaking changes** may occur between minor versions until v1.0 - **Some Hands** are more mature than others (Browser and Researcher are the most battle-tested) @@ -382,6 +382,12 @@ We ship fast and fix fast. The goal is a rock-solid v1.0 by mid-2026. --- +## Security + +To report a security vulnerability, email **jaber@rightnowai.co**. We take all reports seriously and will respond within 48 hours. + +--- + ## License MIT — use it however you want. diff --git a/SECURITY.md b/SECURITY.md index 5e788e51..273d2cab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | |---------|--------------------| -| 0.1.x | :white_check_mark: | +| 0.3.x | :white_check_mark: | ## Reporting a Vulnerability @@ -14,7 +14,7 @@ If you discover a security vulnerability in OpenFang, please report it responsib ### How to Report -1. Email: **security@openfang.ai** +1. Email: **jaber@rightnowai.co** 2. Include: - Description of the vulnerability - Steps to reproduce diff --git a/crates/openfang-api/src/middleware.rs b/crates/openfang-api/src/middleware.rs index 99f12461..3f140eaa 100644 --- a/crates/openfang-api/src/middleware.rs +++ b/crates/openfang-api/src/middleware.rs @@ -87,6 +87,7 @@ pub async fn auth( || (path == "/api/agents" && is_get) || (path == "/api/profiles" && is_get) || (path == "/api/config" && is_get) + || (path == "/api/config/schema" && is_get) || (path.starts_with("/api/uploads/") && is_get) // Dashboard read endpoints — allow unauthenticated so the SPA can // render before the user enters their API key. diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 720b48f8..16fbfefd 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -43,9 +43,49 @@ pub async fn spawn_agent( State(state): State>, Json(req): Json, ) -> impl IntoResponse { + // Resolve template name → manifest_toml if template is provided and manifest_toml is empty + let manifest_toml = if req.manifest_toml.trim().is_empty() { + if let Some(ref tmpl_name) = req.template { + // Sanitize template name to prevent path traversal + let safe_name = tmpl_name + .chars() + .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_') + .collect::(); + if safe_name.is_empty() || safe_name != *tmpl_name { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": "Invalid template name"})), + ); + } + let tmpl_path = state + .kernel + .config + .home_dir + .join("agents") + .join(&safe_name) + .join("agent.toml"); + match std::fs::read_to_string(&tmpl_path) { + Ok(content) => content, + Err(_) => { + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({"error": format!("Template '{}' not found", safe_name)})), + ); + } + } + } else { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": "Either 'manifest_toml' or 'template' is required"})), + ); + } + } else { + req.manifest_toml.clone() + }; + // SECURITY: Reject oversized manifests to prevent parser memory exhaustion. const MAX_MANIFEST_SIZE: usize = 1024 * 1024; // 1MB - if req.manifest_toml.len() > MAX_MANIFEST_SIZE { + if manifest_toml.len() > MAX_MANIFEST_SIZE { return ( StatusCode::PAYLOAD_TOO_LARGE, Json(serde_json::json!({"error": "Manifest too large (max 1MB)"})), @@ -57,7 +97,7 @@ pub async fn spawn_agent( match state.kernel.verify_signed_manifest(signed_json) { Ok(verified_toml) => { // Ensure the signed manifest matches the provided manifest_toml - if verified_toml.trim() != req.manifest_toml.trim() { + if verified_toml.trim() != manifest_toml.trim() { tracing::warn!("Signed manifest content does not match manifest_toml"); return ( StatusCode::BAD_REQUEST, @@ -83,7 +123,7 @@ pub async fn spawn_agent( } } - let manifest: AgentManifest = match toml::from_str(&req.manifest_toml) { + let manifest: AgentManifest = match toml::from_str(&manifest_toml) { Ok(m) => m, Err(e) => { tracing::warn!("Invalid manifest TOML: {e}"); @@ -8941,7 +8981,8 @@ pub async fn config_schema( Json(serde_json::json!({ "sections": { - "api": { + "general": { + "root_level": true, "fields": { "api_listen": "string", "api_key": "string", diff --git a/crates/openfang-api/src/types.rs b/crates/openfang-api/src/types.rs index 9245e6b7..80b71140 100644 --- a/crates/openfang-api/src/types.rs +++ b/crates/openfang-api/src/types.rs @@ -2,11 +2,16 @@ use serde::{Deserialize, Serialize}; -/// Request to spawn an agent from a TOML manifest string. +/// Request to spawn an agent from a TOML manifest string or a template name. #[derive(Debug, Deserialize)] pub struct SpawnRequest { - /// Agent manifest as TOML string. + /// Agent manifest as TOML string (optional if `template` is provided). + #[serde(default)] pub manifest_toml: String, + /// Template name from `~/.openfang/agents/{template}/agent.toml`. + /// When provided and `manifest_toml` is empty, the template is loaded automatically. + #[serde(default)] + pub template: Option, /// Optional Ed25519 signed manifest envelope (JSON). /// When present, the signature is verified before spawning. #[serde(default)] diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index a05fabfe..03b92424 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -4,7 +4,8 @@

API Key Required

-

This instance requires an API key. Enter the key from your config.toml.

+

This instance requires an API key. Enter the key from your config.toml.

+

Add api_key = "your-key" at the top of ~/.openfang/config.toml (not under any [section]).

diff --git a/crates/openfang-api/static/js/pages/settings.js b/crates/openfang-api/static/js/pages/settings.js index 9454313f..54861a6f 100644 --- a/crates/openfang-api/static/js/pages/settings.js +++ b/crates/openfang-api/static/js/pages/settings.js @@ -295,11 +295,14 @@ function settingsPage() { async saveConfigField(section, field, value) { var key = section + '.' + field; + // Root-level fields (api_key, api_listen, log_level) use just the field name + var sectionMeta = this.configSchema && this.configSchema[section]; + var path = (sectionMeta && sectionMeta.root_level) ? field : key; this.configSaving[key] = true; try { - await OpenFangAPI.post('/api/config/set', { path: key, value: value }); + await OpenFangAPI.post('/api/config/set', { path: path, value: value }); this.configDirty[key] = false; - OpenFangToast.success('Saved ' + key); + OpenFangToast.success('Saved ' + field); } catch(e) { OpenFangToast.error('Failed to save: ' + e.message); } diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index 322a1fd6..78cfdbec 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -1163,7 +1163,9 @@ fn cmd_init(quick: bool) { if quick { cmd_init_quick(&openfang_dir); - } else if !std::io::IsTerminal::is_terminal(&std::io::stdout()) { + } else if !std::io::IsTerminal::is_terminal(&std::io::stdin()) + || !std::io::IsTerminal::is_terminal(&std::io::stdout()) + { ui::hint("Non-interactive terminal detected — running in quick mode"); ui::hint("For the interactive wizard, run: openfang init (in a terminal)"); cmd_init_quick(&openfang_dir); @@ -2707,12 +2709,18 @@ decay_rate = 0.05 match client.get(format!("{base}/api/integrations/health")).send() { Ok(resp) if resp.status().is_success() => { if let Ok(body) = resp.json::() { - if let Some(obj) = body.as_object() { - let healthy = obj - .values() - .filter(|v| v.get("healthy").and_then(|h| h.as_bool()).unwrap_or(false)) + let entries = body.get("health").and_then(|h| h.as_array()); + if let Some(arr) = entries { + let healthy = arr + .iter() + .filter(|v| { + v.get("status") + .and_then(|s| s.as_str()) + .map(|s| s.eq_ignore_ascii_case("ready")) + .unwrap_or(false) + }) .count(); - let total = obj.len(); + let total = arr.len(); if healthy == total { if !json { ui::check_ok(&format!( diff --git a/crates/openfang-cli/src/mcp.rs b/crates/openfang-cli/src/mcp.rs index 5cf5bac8..6eb1a807 100644 --- a/crates/openfang-cli/src/mcp.rs +++ b/crates/openfang-cli/src/mcp.rs @@ -230,6 +230,10 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { let method = msg["method"].as_str().unwrap_or(""); let id = msg.get("id").cloned(); + // Per JSON-RPC 2.0 spec: requests MUST have an id field. + // Use null if missing so we always send a response. + let rid = id.unwrap_or(Value::Null); + match method { "initialize" => { let result = json!({ @@ -242,7 +246,7 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { "version": env!("CARGO_PKG_VERSION") } }); - Some(jsonrpc_response(id?, result)) + Some(jsonrpc_response(rid, result)) } "notifications/initialized" => None, // Notification, no response @@ -274,7 +278,7 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { }) }) .collect(); - Some(jsonrpc_response(id?, json!({ "tools": tools }))) + Some(jsonrpc_response(rid, json!({ "tools": tools }))) } "tools/call" => { @@ -286,14 +290,14 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { .to_string(); if message.is_empty() { - return Some(jsonrpc_error(id?, -32602, "Missing 'message' argument")); + return Some(jsonrpc_error(rid, -32602, "Missing 'message' argument")); } let agent_id = match backend.resolve_tool_agent(tool_name) { Some(id) => id, None => { return Some(jsonrpc_error( - id?, + rid, -32602, &format!("Unknown tool: {tool_name}"), )); @@ -302,7 +306,7 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { match backend.send_message(&agent_id, &message) { Ok(response) => Some(jsonrpc_response( - id?, + rid, json!({ "content": [{ "type": "text", @@ -311,7 +315,7 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { }), )), Err(e) => Some(jsonrpc_response( - id?, + rid, json!({ "content": [{ "type": "text", @@ -324,8 +328,8 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option { } _ => { - // Unknown method - id.map(|id| jsonrpc_error(id, -32601, &format!("Method not found: {method}"))) + // Unknown method — always respond with error + Some(jsonrpc_error(rid, -32601, &format!("Method not found: {method}"))) } } } diff --git a/crates/openfang-cli/src/tui/screens/init_wizard.rs b/crates/openfang-cli/src/tui/screens/init_wizard.rs index 07f4c2a2..fb73578e 100644 --- a/crates/openfang-cli/src/tui/screens/init_wizard.rs +++ b/crates/openfang-cli/src/tui/screens/init_wizard.rs @@ -564,6 +564,13 @@ fn tier_label(tier: ModelTier) -> &'static str { // ── Entry point ──────────────────────────────────────────────────────────── pub fn run() -> InitResult { + // Guard against non-TTY environments (Docker, piped, CI/CD) + if !std::io::IsTerminal::is_terminal(&std::io::stdin()) + || !std::io::IsTerminal::is_terminal(&std::io::stdout()) + { + return InitResult::Cancelled; + } + let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { ratatui::restore(); diff --git a/crates/openfang-extensions/integrations/aws.toml b/crates/openfang-extensions/integrations/aws.toml index 0a9768c3..1155499e 100644 --- a/crates/openfang-extensions/integrations/aws.toml +++ b/crates/openfang-extensions/integrations/aws.toml @@ -8,7 +8,7 @@ tags = ["cloud", "amazon", "infrastructure", "s3", "ec2", "lambda", "devops"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-aws"] +args = ["-y", "@aws-mcp/server-aws"] [[required_env]] name = "AWS_ACCESS_KEY_ID" diff --git a/crates/openfang-extensions/integrations/azure-mcp.toml b/crates/openfang-extensions/integrations/azure-mcp.toml index cbd0b98e..b7959e2e 100644 --- a/crates/openfang-extensions/integrations/azure-mcp.toml +++ b/crates/openfang-extensions/integrations/azure-mcp.toml @@ -8,7 +8,7 @@ tags = ["cloud", "microsoft", "infrastructure", "azure", "devops", "enterprise"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-azure"] +args = ["-y", "@azure/mcp@latest", "server", "start"] [[required_env]] name = "AZURE_SUBSCRIPTION_ID" diff --git a/crates/openfang-extensions/integrations/bitbucket.toml b/crates/openfang-extensions/integrations/bitbucket.toml index 205040f9..3ba39524 100644 --- a/crates/openfang-extensions/integrations/bitbucket.toml +++ b/crates/openfang-extensions/integrations/bitbucket.toml @@ -8,7 +8,7 @@ tags = ["git", "vcs", "code", "pull-requests", "ci", "atlassian"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-bitbucket"] +args = ["-y", "@atlassian-mcp-server/bitbucket"] [[required_env]] name = "BITBUCKET_USERNAME" diff --git a/crates/openfang-extensions/integrations/brave-search.toml b/crates/openfang-extensions/integrations/brave-search.toml index 8f1885c6..d5da51af 100644 --- a/crates/openfang-extensions/integrations/brave-search.toml +++ b/crates/openfang-extensions/integrations/brave-search.toml @@ -8,7 +8,7 @@ tags = ["search", "web", "brave", "api", "information-retrieval"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-brave-search"] +args = ["-y", "@modelcontextprotocol/server-brave-search"] [[required_env]] name = "BRAVE_API_KEY" diff --git a/crates/openfang-extensions/integrations/discord-mcp.toml b/crates/openfang-extensions/integrations/discord-mcp.toml index 2726b1e1..2cdf7734 100644 --- a/crates/openfang-extensions/integrations/discord-mcp.toml +++ b/crates/openfang-extensions/integrations/discord-mcp.toml @@ -8,7 +8,7 @@ tags = ["chat", "messaging", "community", "gaming", "voice"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-discord"] +args = ["-y", "mcp-discord"] [[required_env]] name = "DISCORD_BOT_TOKEN" diff --git a/crates/openfang-extensions/integrations/dropbox.toml b/crates/openfang-extensions/integrations/dropbox.toml index 01bb2b2e..8b219b90 100644 --- a/crates/openfang-extensions/integrations/dropbox.toml +++ b/crates/openfang-extensions/integrations/dropbox.toml @@ -8,7 +8,7 @@ tags = ["files", "storage", "cloud-storage", "sync", "sharing"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-dropbox"] +args = ["-y", "@microagents/mcp-server-dropbox"] [[required_env]] name = "DROPBOX_ACCESS_TOKEN" diff --git a/crates/openfang-extensions/integrations/elasticsearch.toml b/crates/openfang-extensions/integrations/elasticsearch.toml index 9c64159b..fea180f1 100644 --- a/crates/openfang-extensions/integrations/elasticsearch.toml +++ b/crates/openfang-extensions/integrations/elasticsearch.toml @@ -8,7 +8,7 @@ tags = ["search", "database", "indexing", "analytics", "full-text"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-elasticsearch"] +args = ["-y", "@elastic/mcp-server-elasticsearch"] [[required_env]] name = "ELASTICSEARCH_URL" diff --git a/crates/openfang-extensions/integrations/exa-search.toml b/crates/openfang-extensions/integrations/exa-search.toml index ce957935..737b5973 100644 --- a/crates/openfang-extensions/integrations/exa-search.toml +++ b/crates/openfang-extensions/integrations/exa-search.toml @@ -8,7 +8,7 @@ tags = ["search", "web", "ai", "neural", "semantic", "information-retrieval"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-exa"] +args = ["-y", "exa-mcp-server"] [[required_env]] name = "EXA_API_KEY" diff --git a/crates/openfang-extensions/integrations/gcp-mcp.toml b/crates/openfang-extensions/integrations/gcp-mcp.toml index d93cc3a5..43b923f1 100644 --- a/crates/openfang-extensions/integrations/gcp-mcp.toml +++ b/crates/openfang-extensions/integrations/gcp-mcp.toml @@ -8,7 +8,7 @@ tags = ["cloud", "google", "infrastructure", "gce", "gcs", "bigquery", "devops"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-gcp"] +args = ["-y", "@google-cloud/gcloud-mcp"] [[required_env]] name = "GOOGLE_APPLICATION_CREDENTIALS" diff --git a/crates/openfang-extensions/integrations/github.toml b/crates/openfang-extensions/integrations/github.toml index f4cac762..17cdde2a 100644 --- a/crates/openfang-extensions/integrations/github.toml +++ b/crates/openfang-extensions/integrations/github.toml @@ -8,7 +8,7 @@ tags = ["git", "vcs", "code", "issues", "pull-requests", "ci"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-github"] +args = ["-y", "@modelcontextprotocol/server-github"] [[required_env]] name = "GITHUB_PERSONAL_ACCESS_TOKEN" diff --git a/crates/openfang-extensions/integrations/gitlab.toml b/crates/openfang-extensions/integrations/gitlab.toml index 14724057..1df602ad 100644 --- a/crates/openfang-extensions/integrations/gitlab.toml +++ b/crates/openfang-extensions/integrations/gitlab.toml @@ -8,7 +8,7 @@ tags = ["git", "vcs", "code", "merge-requests", "ci", "devops"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-gitlab"] +args = ["-y", "@modelcontextprotocol/server-gitlab"] [[required_env]] name = "GITLAB_PERSONAL_ACCESS_TOKEN" diff --git a/crates/openfang-extensions/integrations/gmail.toml b/crates/openfang-extensions/integrations/gmail.toml index d9e2e30c..c847b3f6 100644 --- a/crates/openfang-extensions/integrations/gmail.toml +++ b/crates/openfang-extensions/integrations/gmail.toml @@ -8,7 +8,7 @@ tags = ["email", "google", "messaging", "inbox", "communication"] [transport] type = "stdio" command = "npx" -args = ["@anthropic/server-gmail"] +args = ["-y", "@gongrzhe/server-gmail-autoauth-mcp"] [oauth] provider = "google" diff --git a/crates/openfang-extensions/integrations/google-calendar.toml b/crates/openfang-extensions/integrations/google-calendar.toml index b9a94b71..db84ada8 100644 --- a/crates/openfang-extensions/integrations/google-calendar.toml +++ b/crates/openfang-extensions/integrations/google-calendar.toml @@ -8,7 +8,7 @@ tags = ["calendar", "scheduling", "google", "events", "meetings"] [transport] type = "stdio" command = "npx" -args = ["@anthropic/server-google-calendar"] +args = ["-y", "@cocal/google-calendar-mcp"] [oauth] provider = "google" diff --git a/crates/openfang-extensions/integrations/google-drive.toml b/crates/openfang-extensions/integrations/google-drive.toml index c106eb88..f3198866 100644 --- a/crates/openfang-extensions/integrations/google-drive.toml +++ b/crates/openfang-extensions/integrations/google-drive.toml @@ -8,7 +8,7 @@ tags = ["files", "storage", "google", "documents", "cloud-storage"] [transport] type = "stdio" command = "npx" -args = ["@anthropic/server-google-drive"] +args = ["-y", "@modelcontextprotocol/server-gdrive"] [oauth] provider = "google" diff --git a/crates/openfang-extensions/integrations/jira.toml b/crates/openfang-extensions/integrations/jira.toml index ad3efb24..2afecb01 100644 --- a/crates/openfang-extensions/integrations/jira.toml +++ b/crates/openfang-extensions/integrations/jira.toml @@ -8,7 +8,7 @@ tags = ["project-management", "issues", "agile", "atlassian", "tracking"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-atlassian"] +args = ["-y", "@aashari/mcp-server-atlassian-jira"] [[required_env]] name = "JIRA_API_TOKEN" diff --git a/crates/openfang-extensions/integrations/linear.toml b/crates/openfang-extensions/integrations/linear.toml index c7342c7b..1faca179 100644 --- a/crates/openfang-extensions/integrations/linear.toml +++ b/crates/openfang-extensions/integrations/linear.toml @@ -8,7 +8,7 @@ tags = ["project-management", "issues", "agile", "tracking", "sprint"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-linear"] +args = ["-y", "linear-mcp"] [[required_env]] name = "LINEAR_API_KEY" diff --git a/crates/openfang-extensions/integrations/mongodb.toml b/crates/openfang-extensions/integrations/mongodb.toml index a5e716aa..260e2003 100644 --- a/crates/openfang-extensions/integrations/mongodb.toml +++ b/crates/openfang-extensions/integrations/mongodb.toml @@ -8,7 +8,7 @@ tags = ["database", "nosql", "document", "mongo", "queries"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-mongodb"] +args = ["-y", "@mongodb-js/mongodb-mcp-server"] [[required_env]] name = "MONGODB_URI" diff --git a/crates/openfang-extensions/integrations/notion.toml b/crates/openfang-extensions/integrations/notion.toml index 892bfb19..8561b720 100644 --- a/crates/openfang-extensions/integrations/notion.toml +++ b/crates/openfang-extensions/integrations/notion.toml @@ -8,7 +8,7 @@ tags = ["notes", "wiki", "knowledge-base", "documentation", "databases"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-notion"] +args = ["-y", "@notionhq/notion-mcp-server"] [[required_env]] name = "NOTION_API_KEY" diff --git a/crates/openfang-extensions/integrations/postgresql.toml b/crates/openfang-extensions/integrations/postgresql.toml index f1f59419..225da8f7 100644 --- a/crates/openfang-extensions/integrations/postgresql.toml +++ b/crates/openfang-extensions/integrations/postgresql.toml @@ -8,7 +8,7 @@ tags = ["database", "sql", "relational", "postgres", "queries"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-postgres"] +args = ["-y", "@modelcontextprotocol/server-postgres"] [[required_env]] name = "POSTGRES_CONNECTION_STRING" diff --git a/crates/openfang-extensions/integrations/redis.toml b/crates/openfang-extensions/integrations/redis.toml index 243d10e2..e3ad7ad4 100644 --- a/crates/openfang-extensions/integrations/redis.toml +++ b/crates/openfang-extensions/integrations/redis.toml @@ -8,7 +8,7 @@ tags = ["database", "cache", "key-value", "in-memory", "nosql"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-redis"] +args = ["-y", "@modelcontextprotocol/server-redis"] [[required_env]] name = "REDIS_URL" diff --git a/crates/openfang-extensions/integrations/sentry.toml b/crates/openfang-extensions/integrations/sentry.toml index 97379dd9..b8c9b7c6 100644 --- a/crates/openfang-extensions/integrations/sentry.toml +++ b/crates/openfang-extensions/integrations/sentry.toml @@ -8,7 +8,7 @@ tags = ["monitoring", "errors", "debugging", "observability", "apm"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-sentry"] +args = ["-y", "@sentry/mcp-server"] [[required_env]] name = "SENTRY_AUTH_TOKEN" diff --git a/crates/openfang-extensions/integrations/slack.toml b/crates/openfang-extensions/integrations/slack.toml index 660649ef..ad95627b 100644 --- a/crates/openfang-extensions/integrations/slack.toml +++ b/crates/openfang-extensions/integrations/slack.toml @@ -8,7 +8,7 @@ tags = ["chat", "messaging", "team", "channels", "collaboration"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-slack"] +args = ["-y", "@modelcontextprotocol/server-slack"] [[required_env]] name = "SLACK_BOT_TOKEN" diff --git a/crates/openfang-extensions/integrations/sqlite-mcp.toml b/crates/openfang-extensions/integrations/sqlite-mcp.toml index fcfd9203..ca4f1870 100644 --- a/crates/openfang-extensions/integrations/sqlite-mcp.toml +++ b/crates/openfang-extensions/integrations/sqlite-mcp.toml @@ -8,7 +8,7 @@ tags = ["database", "sql", "relational", "sqlite", "local", "embedded"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-sqlite"] +args = ["-y", "@modelcontextprotocol/server-sqlite"] [[required_env]] name = "SQLITE_DB_PATH" diff --git a/crates/openfang-extensions/integrations/teams-mcp.toml b/crates/openfang-extensions/integrations/teams-mcp.toml index 3cb7831e..b47d70bf 100644 --- a/crates/openfang-extensions/integrations/teams-mcp.toml +++ b/crates/openfang-extensions/integrations/teams-mcp.toml @@ -8,7 +8,7 @@ tags = ["chat", "messaging", "microsoft", "enterprise", "collaboration"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-teams"] +args = ["-y", "teams-mcp"] [oauth] provider = "microsoft" diff --git a/crates/openfang-extensions/integrations/todoist.toml b/crates/openfang-extensions/integrations/todoist.toml index 4c4d9fbf..3785975f 100644 --- a/crates/openfang-extensions/integrations/todoist.toml +++ b/crates/openfang-extensions/integrations/todoist.toml @@ -8,7 +8,7 @@ tags = ["tasks", "todo", "project-management", "productivity", "gtd"] [transport] type = "stdio" command = "npx" -args = ["@modelcontextprotocol/server-todoist"] +args = ["-y", "todoist-mcp"] [[required_env]] name = "TODOIST_API_KEY" diff --git a/crates/openfang-kernel/src/config.rs b/crates/openfang-kernel/src/config.rs index f77d6ac3..c518d85b 100644 --- a/crates/openfang-kernel/src/config.rs +++ b/crates/openfang-kernel/src/config.rs @@ -50,6 +50,25 @@ pub fn load_config(path: Option<&Path>) -> KernelConfig { tbl.remove("include"); } + // Migrate misplaced api_key/api_listen from [api] section to root level. + // The old config schema incorrectly grouped these under [api], so many + // users have them in the wrong place. Move them up if not already at root. + if let toml::Value::Table(ref mut tbl) = root_value { + if let Some(toml::Value::Table(api_section)) = tbl.get("api").cloned() { + for key in &["api_key", "api_listen", "log_level"] { + if !tbl.contains_key(*key) { + if let Some(val) = api_section.get(*key) { + tracing::info!( + key, + "Migrating misplaced config field from [api] to root level" + ); + tbl.insert(key.to_string(), val.clone()); + } + } + } + } + } + match root_value.try_into::() { Ok(config) => { info!(path = %config_path.display(), "Loaded configuration"); diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 100ccf8a..ca420f3c 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -4713,8 +4713,8 @@ fn apply_budget_defaults( budget: &openfang_types::config::BudgetConfig, resources: &mut ResourceQuota, ) { - // Only override hourly if agent has the built-in default (1.0) and global is set - if budget.max_hourly_usd > 0.0 && resources.max_cost_per_hour_usd == 1.0 { + // Only override hourly if agent has unlimited (0.0) and global is set + if budget.max_hourly_usd > 0.0 && resources.max_cost_per_hour_usd == 0.0 { resources.max_cost_per_hour_usd = budget.max_hourly_usd; } // Only override daily/monthly if agent has unlimited (0.0) and global is set diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index 11fa8ea1..327f9d64 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -706,6 +706,23 @@ pub async fn run_agent_loop( }); } + // Detect tool errors and inject guidance to prevent fabrication + let error_count = tool_result_blocks.iter().filter(|b| { + matches!(b, ContentBlock::ToolResult { is_error: true, .. }) + }).count(); + let non_denial_errors = error_count.saturating_sub(denial_count); + if non_denial_errors > 0 { + tool_result_blocks.push(ContentBlock::Text { + text: format!( + "[System: {} tool(s) returned errors. Report the error honestly \ + to the user. Do NOT fabricate results or pretend the tool succeeded. \ + If a search or fetch failed, tell the user it failed and suggest \ + alternatives instead of making up data.]", + non_denial_errors + ), + }); + } + // Add tool results as a user message (Anthropic API requirement) let tool_results_msg = Message { role: Role::User, @@ -1629,6 +1646,23 @@ pub async fn run_agent_loop_streaming( }); } + // Detect tool errors and inject guidance to prevent fabrication + let error_count = tool_result_blocks.iter().filter(|b| { + matches!(b, ContentBlock::ToolResult { is_error: true, .. }) + }).count(); + let non_denial_errors = error_count.saturating_sub(denial_count); + if non_denial_errors > 0 { + tool_result_blocks.push(ContentBlock::Text { + text: format!( + "[System: {} tool(s) returned errors. Report the error honestly \ + to the user. Do NOT fabricate results or pretend the tool succeeded. \ + If a search or fetch failed, tell the user it failed and suggest \ + alternatives instead of making up data.]", + non_denial_errors + ), + }); + } + let tool_results_msg = Message { role: Role::User, content: MessageContent::Blocks(tool_result_blocks.clone()), diff --git a/crates/openfang-skills/src/clawhub.rs b/crates/openfang-skills/src/clawhub.rs index 3f9129cb..6401643c 100644 --- a/crates/openfang-skills/src/clawhub.rs +++ b/crates/openfang-skills/src/clawhub.rs @@ -427,25 +427,45 @@ impl ClawHubClient { info!(slug, "Downloading skill from ClawHub"); - let response = self - .client - .get(&url) - .header("User-Agent", "OpenFang/0.1") - .send() - .await - .map_err(|e| SkillError::Network(format!("ClawHub download failed: {e}")))?; - - if !response.status().is_success() { - return Err(SkillError::Network(format!( - "ClawHub download returned {}", - response.status() - ))); + // Retry with exponential backoff on 429/5xx + let mut last_err = String::new(); + let mut bytes_result = None; + for attempt in 0..3u32 { + if attempt > 0 { + let delay = std::time::Duration::from_millis(1000 * 2u64.pow(attempt)); + tokio::time::sleep(delay).await; + info!(slug, attempt, "Retrying ClawHub download"); + } + match self + .client + .get(&url) + .header("User-Agent", "OpenFang/0.1") + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + match resp.bytes().await { + Ok(b) => { + bytes_result = Some(b); + break; + } + Err(e) => last_err = format!("Failed to read download: {e}"), + } + } + Ok(resp) if resp.status().as_u16() == 429 || resp.status().is_server_error() => { + last_err = format!("ClawHub download returned {}", resp.status()); + } + Ok(resp) => { + return Err(SkillError::Network(format!( + "ClawHub download returned {}", + resp.status() + ))); + } + Err(e) => last_err = format!("ClawHub download failed: {e}"), + } } - - let bytes = response - .bytes() - .await - .map_err(|e| SkillError::Network(format!("Failed to read download: {e}")))?; + let bytes = bytes_result + .ok_or_else(|| SkillError::Network(format!("{last_err} (after 3 attempts)")))?; // Step 1: SHA256 of downloaded content let sha256 = { diff --git a/docs/security.md b/docs/security.md index b119a4bd..24163d8a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,5 +1,7 @@ # OpenFang Security Architecture +> **Security Contact:** jaber@rightnowai.co — Report vulnerabilities via email. We respond within 48 hours. + This document provides a comprehensive technical reference for every security system in the OpenFang Agent Operating System. All struct names, function signatures, constant values, and algorithm descriptions are drawn directly from diff --git a/scripts/install.sh b/scripts/install.sh index 7e645bf5..439a3fec 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -111,8 +111,22 @@ install() { chmod +x "$INSTALL_DIR/openfang" # Ad-hoc codesign on macOS (prevents SIGKILL on Apple Silicon) - if [ "$OS" = "darwin" ] && command -v codesign &>/dev/null; then - codesign --force --sign - "$INSTALL_DIR/openfang" 2>/dev/null || true + # Must strip extended attributes (com.apple.quarantine) BEFORE signing, + # otherwise the signature is computed over the quarantine xattr and macOS + # rejects it as "Code Signature Invalid" → SIGKILL. + if [ "$OS" = "darwin" ]; then + if command -v xattr &>/dev/null; then + xattr -cr "$INSTALL_DIR/openfang" 2>/dev/null || true + fi + if command -v codesign &>/dev/null; then + if ! codesign --force --sign - "$INSTALL_DIR/openfang"; then + echo "" + echo " Warning: ad-hoc code signing failed." + echo " On Apple Silicon, the binary may be killed (SIGKILL) by Gatekeeper." + echo " Try manually: xattr -cr $INSTALL_DIR/openfang && codesign --force --sign - $INSTALL_DIR/openfang" + echo "" + fi + fi fi # Add to PATH — detect the user's login shell