mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-08-14 00:47:51 +00:00
community fixes
This commit is contained in:
@@ -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: |
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/language-Rust-orange?style=flat-square" alt="Rust" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT" />
|
||||
<img src="https://img.shields.io/badge/version-0.1.0-green?style=flat-square" alt="v0.1.0" />
|
||||
<img src="https://img.shields.io/badge/version-0.3.30-green?style=flat-square" alt="v0.3.30" />
|
||||
<img src="https://img.shields.io/badge/tests-1,767%2B%20passing-brightgreen?style=flat-square" alt="Tests" />
|
||||
<img src="https://img.shields.io/badge/clippy-0%20warnings-brightgreen?style=flat-square" alt="Clippy" />
|
||||
<a href="https://www.buymeacoffee.com/openfang" target="_blank"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=flat-square&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee" /></a>
|
||||
@@ -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.
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -43,9 +43,49 @@ pub async fn spawn_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SpawnRequest>,
|
||||
) -> 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::<String>();
|
||||
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",
|
||||
|
||||
@@ -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<String>,
|
||||
/// Optional Ed25519 signed manifest envelope (JSON).
|
||||
/// When present, the signature is verified before spawning.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<div x-show="$store.app.showAuthPrompt" style="position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px)" x-data="{ apiKeyInput: '' }">
|
||||
<div style="background:var(--bg-card,#1e1e2e);border:1px solid var(--border,#333);border-radius:12px;padding:2rem;max-width:400px;width:90%">
|
||||
<h3 style="margin:0 0 0.5rem;font-size:1.1rem">API Key Required</h3>
|
||||
<p style="color:var(--text-dim,#888);font-size:0.85rem;margin:0 0 1rem">This instance requires an API key. Enter the key from your <code>config.toml</code>.</p>
|
||||
<p style="color:var(--text-dim,#888);font-size:0.85rem;margin:0 0 0.5rem">This instance requires an API key. Enter the key from your <code>config.toml</code>.</p>
|
||||
<p style="color:var(--text-dim,#666);font-size:0.75rem;margin:0 0 1rem">Add <code style="color:var(--accent-light,#a78bfa);background:var(--bg,#111);padding:1px 4px;border-radius:2px">api_key = "your-key"</code> at the <strong>top</strong> of <code>~/.openfang/config.toml</code> (not under any [section]).</p>
|
||||
<input type="password" x-model="apiKeyInput" placeholder="Enter API key..." @keydown.enter="$store.app.submitApiKey(apiKeyInput)" style="width:100%;padding:0.6rem;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#151520);color:var(--text,#e0e0e0);font-size:0.9rem;box-sizing:border-box;margin-bottom:0.75rem">
|
||||
<button @click="$store.app.submitApiKey(apiKeyInput)" style="width:100%;padding:0.6rem;border-radius:6px;border:none;background:var(--accent,#7c3aed);color:#fff;font-weight:600;cursor:pointer;font-size:0.9rem">Unlock Dashboard</button>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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::<serde_json::Value>() {
|
||||
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!(
|
||||
|
||||
@@ -230,6 +230,10 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option<Value> {
|
||||
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<Value> {
|
||||
"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<Value> {
|
||||
})
|
||||
})
|
||||
.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<Value> {
|
||||
.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<Value> {
|
||||
|
||||
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<Value> {
|
||||
}),
|
||||
)),
|
||||
Err(e) => Some(jsonrpc_response(
|
||||
id?,
|
||||
rid,
|
||||
json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
@@ -324,8 +328,8 @@ fn handle_message(backend: &McpBackend, msg: &Value) -> Option<Value> {
|
||||
}
|
||||
|
||||
_ => {
|
||||
// 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}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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::<KernelConfig>() {
|
||||
Ok(config) => {
|
||||
info!(path = %config_path.display(), "Loaded configuration");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
+16
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user