mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-04-25 17:25:11 +02:00
fix issues
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
<p align="center">
|
||||
Open-source Agent OS built in Rust. 137K LOC. 14 crates. 1,767+ tests. Zero clippy warnings.<br/>
|
||||
<strong>One binary. Production-grade. Agents that actually work for you.</strong>
|
||||
<strong>One binary. Battle-tested. Agents that actually work for you.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
## What is OpenFang?
|
||||
|
||||
OpenFang is a **production-grade Agent Operating System** — not a chatbot framework, not a Python wrapper around an LLM, not a "multi-agent orchestrator." It is a full operating system for autonomous agents, built from scratch in Rust.
|
||||
OpenFang is an **open-source Agent Operating System** — not a chatbot framework, not a Python wrapper around an LLM, not a "multi-agent orchestrator." It is a full operating system for autonomous agents, built from scratch in Rust.
|
||||
|
||||
Traditional agent frameworks wait for you to type something. OpenFang runs **autonomous agents that work for you** — on schedules, 24/7, building knowledge graphs, monitoring targets, generating leads, managing your social media, and reporting results to your dashboard.
|
||||
|
||||
@@ -370,7 +370,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 production-grade. That said:
|
||||
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:
|
||||
|
||||
- **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)
|
||||
|
||||
@@ -31,6 +31,8 @@ pub struct AppState {
|
||||
pub bridge_manager: tokio::sync::Mutex<Option<openfang_channels::bridge::BridgeManager>>,
|
||||
/// Live channel config — updated on every hot-reload so list_channels() reflects reality.
|
||||
pub channels_config: tokio::sync::RwLock<openfang_types::config::ChannelsConfig>,
|
||||
/// Notify handle to trigger graceful HTTP server shutdown from the API.
|
||||
pub shutdown_notify: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
/// POST /api/agents — Spawn a new agent.
|
||||
@@ -492,6 +494,8 @@ pub async fn shutdown(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
"ok",
|
||||
);
|
||||
state.kernel.shutdown();
|
||||
// Signal the HTTP server to initiate graceful shutdown so the process exits.
|
||||
state.shutdown_notify.notify_one();
|
||||
Json(serde_json::json!({"status": "shutting_down"}))
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ pub async fn build_router(
|
||||
peer_registry: kernel.peer_registry.as_ref().map(|r| Arc::new(r.clone())),
|
||||
bridge_manager: tokio::sync::Mutex::new(bridge),
|
||||
channels_config: tokio::sync::RwLock::new(channels_config),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
// CORS: allow localhost origins by default. If API key is set, the API
|
||||
@@ -710,11 +711,12 @@ pub async fn run_daemon(
|
||||
// Run server with graceful shutdown.
|
||||
// SECURITY: `into_make_service_with_connect_info` injects the peer
|
||||
// SocketAddr so the auth middleware can check for loopback connections.
|
||||
let api_shutdown = state.shutdown_notify.clone();
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.with_graceful_shutdown(shutdown_signal(api_shutdown))
|
||||
.await?;
|
||||
|
||||
// Clean up daemon info file
|
||||
@@ -752,11 +754,11 @@ pub fn read_daemon_info(home_dir: &Path) -> Option<DaemonInfo> {
|
||||
serde_json::from_str(&contents).ok()
|
||||
}
|
||||
|
||||
/// Wait for an OS termination signal.
|
||||
/// Wait for an OS termination signal OR an API shutdown request.
|
||||
///
|
||||
/// On Unix: listens for SIGINT and SIGTERM.
|
||||
/// On Windows: listens for Ctrl+C.
|
||||
async fn shutdown_signal() {
|
||||
/// On Unix: listens for SIGINT, SIGTERM, and API notify.
|
||||
/// On Windows: listens for Ctrl+C and API notify.
|
||||
async fn shutdown_signal(api_shutdown: Arc<tokio::sync::Notify>) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
@@ -770,15 +772,22 @@ async fn shutdown_signal() {
|
||||
_ = sigterm.recv() => {
|
||||
info!("Received SIGTERM, shutting down...");
|
||||
}
|
||||
_ = api_shutdown.notified() => {
|
||||
info!("Shutdown requested via API, shutting down...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install Ctrl+C handler");
|
||||
info!("Ctrl+C received, shutting down...");
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Ctrl+C received, shutting down...");
|
||||
}
|
||||
_ = api_shutdown.notified() => {
|
||||
info!("Shutdown requested via API, shutting down...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ async fn start_test_server_with_provider(
|
||||
peer_registry: None,
|
||||
bridge_manager: tokio::sync::Mutex::new(None),
|
||||
channels_config: tokio::sync::RwLock::new(Default::default()),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
@@ -700,6 +701,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer {
|
||||
peer_registry: None,
|
||||
bridge_manager: tokio::sync::Mutex::new(None),
|
||||
channels_config: tokio::sync::RwLock::new(Default::default()),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
let api_key_state = state.kernel.config.api_key.clone();
|
||||
|
||||
@@ -112,6 +112,7 @@ async fn test_full_daemon_lifecycle() {
|
||||
peer_registry: None,
|
||||
bridge_manager: tokio::sync::Mutex::new(None),
|
||||
channels_config: tokio::sync::RwLock::new(Default::default()),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
@@ -234,6 +235,7 @@ async fn test_server_immediate_responsiveness() {
|
||||
peer_registry: None,
|
||||
bridge_manager: tokio::sync::Mutex::new(None),
|
||||
channels_config: tokio::sync::RwLock::new(Default::default()),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
|
||||
@@ -56,6 +56,7 @@ async fn start_test_server() -> TestServer {
|
||||
peer_registry: None,
|
||||
bridge_manager: tokio::sync::Mutex::new(None),
|
||||
channels_config: tokio::sync::RwLock::new(Default::default()),
|
||||
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
|
||||
@@ -1332,7 +1332,23 @@ fn cmd_stop() {
|
||||
let client = daemon_client();
|
||||
match client.post(format!("{base}/api/shutdown")).send() {
|
||||
Ok(r) if r.status().is_success() => {
|
||||
ui::success("Daemon is shutting down");
|
||||
// Wait for daemon to actually stop (up to 5 seconds)
|
||||
for _ in 0..10 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
if find_daemon().is_none() {
|
||||
ui::success("Daemon stopped");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Still alive — force kill via PID
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let of_dir = home.join(".openfang");
|
||||
if let Some(info) = read_daemon_info(&of_dir) {
|
||||
force_kill_pid(info.pid);
|
||||
let _ = std::fs::remove_file(of_dir.join("daemon.json"));
|
||||
}
|
||||
}
|
||||
ui::success("Daemon stopped (forced)");
|
||||
}
|
||||
Ok(r) => {
|
||||
ui::error(&format!("Shutdown request failed ({})", r.status()));
|
||||
@@ -1351,6 +1367,21 @@ fn cmd_stop() {
|
||||
}
|
||||
}
|
||||
|
||||
fn force_kill_pid(pid: u32) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = std::process::Command::new("kill")
|
||||
.args(["-9", &pid.to_string()])
|
||||
.output();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/F"])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show context-aware error for kernel boot failures.
|
||||
fn boot_kernel_error(e: &openfang_kernel::error::KernelError) {
|
||||
let msg = e.to_string();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# NOTE: The GHCR image (ghcr.io/rightnow-ai/openfang) is not yet public.
|
||||
# For now, build from source using `docker compose up --build`.
|
||||
# See: https://github.com/RightNow-AI/openfang/issues/12
|
||||
|
||||
version: "3.8"
|
||||
services:
|
||||
openfang:
|
||||
build: .
|
||||
image: ghcr.io/rightnow-ai/openfang:latest
|
||||
# image: ghcr.io/rightnow-ai/openfang:latest # Uncomment when GHCR is public
|
||||
ports:
|
||||
- "4200:4200"
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user