community fixes

This commit is contained in:
jaberjaber23
2026-03-08 22:29:54 +03:00
parent f2413949bc
commit eba9198827
43 changed files with 245 additions and 78 deletions
+3 -1
View File
@@ -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
View File
@@ -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"
+10 -4
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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.
+45 -4
View File
@@ -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",
+7 -2
View File
@@ -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)]
+2 -1
View File
@@ -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);
}
+14 -6
View File
@@ -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!(
+12 -8
View File
@@ -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"
+19
View File
@@ -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");
+2 -2
View File
@@ -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
+34
View File
@@ -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()),
+38 -18
View File
@@ -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 = {
+2
View File
@@ -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
View File
@@ -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