-
+
@@ -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