Skip to content

Commit a1d6776

Browse files
committed
fix issues
1 parent ff40824 commit a1d6776

File tree

8 files changed

+67
-14
lines changed

8 files changed

+67
-14
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<p align="center">
99
Open-source Agent OS built in Rust. 137K LOC. 14 crates. 1,767+ tests. Zero clippy warnings.<br/>
10-
<strong>One binary. Production-grade. Agents that actually work for you.</strong>
10+
<strong>One binary. Battle-tested. Agents that actually work for you.</strong>
1111
</p>
1212

1313
<p align="center">
@@ -34,7 +34,7 @@
3434

3535
## What is OpenFang?
3636

37-
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.
37+
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.
3838

3939
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.
4040

@@ -370,7 +370,7 @@ cargo fmt --all -- --check
370370

371371
## Stability Notice
372372

373-
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:
373+
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:
374374

375375
- **Breaking changes** may occur between minor versions until v1.0
376376
- **Some Hands** are more mature than others (Browser and Researcher are the most battle-tested)

crates/openfang-api/src/routes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub struct AppState {
3131
pub bridge_manager: tokio::sync::Mutex<Option<openfang_channels::bridge::BridgeManager>>,
3232
/// Live channel config — updated on every hot-reload so list_channels() reflects reality.
3333
pub channels_config: tokio::sync::RwLock<openfang_types::config::ChannelsConfig>,
34+
/// Notify handle to trigger graceful HTTP server shutdown from the API.
35+
pub shutdown_notify: Arc<tokio::sync::Notify>,
3436
}
3537

3638
/// POST /api/agents — Spawn a new agent.
@@ -492,6 +494,8 @@ pub async fn shutdown(State(state): State<Arc<AppState>>) -> impl IntoResponse {
492494
"ok",
493495
);
494496
state.kernel.shutdown();
497+
// Signal the HTTP server to initiate graceful shutdown so the process exits.
498+
state.shutdown_notify.notify_one();
495499
Json(serde_json::json!({"status": "shutting_down"}))
496500
}
497501

crates/openfang-api/src/server.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub async fn build_router(
4848
peer_registry: kernel.peer_registry.as_ref().map(|r| Arc::new(r.clone())),
4949
bridge_manager: tokio::sync::Mutex::new(bridge),
5050
channels_config: tokio::sync::RwLock::new(channels_config),
51+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
5152
});
5253

5354
// CORS: allow localhost origins by default. If API key is set, the API
@@ -710,11 +711,12 @@ pub async fn run_daemon(
710711
// Run server with graceful shutdown.
711712
// SECURITY: `into_make_service_with_connect_info` injects the peer
712713
// SocketAddr so the auth middleware can check for loopback connections.
714+
let api_shutdown = state.shutdown_notify.clone();
713715
axum::serve(
714716
listener,
715717
app.into_make_service_with_connect_info::<SocketAddr>(),
716718
)
717-
.with_graceful_shutdown(shutdown_signal())
719+
.with_graceful_shutdown(shutdown_signal(api_shutdown))
718720
.await?;
719721

720722
// Clean up daemon info file
@@ -752,11 +754,11 @@ pub fn read_daemon_info(home_dir: &Path) -> Option<DaemonInfo> {
752754
serde_json::from_str(&contents).ok()
753755
}
754756

755-
/// Wait for an OS termination signal.
757+
/// Wait for an OS termination signal OR an API shutdown request.
756758
///
757-
/// On Unix: listens for SIGINT and SIGTERM.
758-
/// On Windows: listens for Ctrl+C.
759-
async fn shutdown_signal() {
759+
/// On Unix: listens for SIGINT, SIGTERM, and API notify.
760+
/// On Windows: listens for Ctrl+C and API notify.
761+
async fn shutdown_signal(api_shutdown: Arc<tokio::sync::Notify>) {
760762
#[cfg(unix)]
761763
{
762764
use tokio::signal::unix::{signal, SignalKind};
@@ -770,15 +772,22 @@ async fn shutdown_signal() {
770772
_ = sigterm.recv() => {
771773
info!("Received SIGTERM, shutting down...");
772774
}
775+
_ = api_shutdown.notified() => {
776+
info!("Shutdown requested via API, shutting down...");
777+
}
773778
}
774779
}
775780

776781
#[cfg(not(unix))]
777782
{
778-
tokio::signal::ctrl_c()
779-
.await
780-
.expect("Failed to install Ctrl+C handler");
781-
info!("Ctrl+C received, shutting down...");
783+
tokio::select! {
784+
_ = tokio::signal::ctrl_c() => {
785+
info!("Ctrl+C received, shutting down...");
786+
}
787+
_ = api_shutdown.notified() => {
788+
info!("Shutdown requested via API, shutting down...");
789+
}
790+
}
782791
}
783792
}
784793

crates/openfang-api/tests/api_integration_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ async fn start_test_server_with_provider(
7575
peer_registry: None,
7676
bridge_manager: tokio::sync::Mutex::new(None),
7777
channels_config: tokio::sync::RwLock::new(Default::default()),
78+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
7879
});
7980

8081
let app = Router::new()
@@ -700,6 +701,7 @@ async fn start_test_server_with_auth(api_key: &str) -> TestServer {
700701
peer_registry: None,
701702
bridge_manager: tokio::sync::Mutex::new(None),
702703
channels_config: tokio::sync::RwLock::new(Default::default()),
704+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
703705
});
704706

705707
let api_key_state = state.kernel.config.api_key.clone();

crates/openfang-api/tests/daemon_lifecycle_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ async fn test_full_daemon_lifecycle() {
112112
peer_registry: None,
113113
bridge_manager: tokio::sync::Mutex::new(None),
114114
channels_config: tokio::sync::RwLock::new(Default::default()),
115+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
115116
});
116117

117118
let app = Router::new()
@@ -234,6 +235,7 @@ async fn test_server_immediate_responsiveness() {
234235
peer_registry: None,
235236
bridge_manager: tokio::sync::Mutex::new(None),
236237
channels_config: tokio::sync::RwLock::new(Default::default()),
238+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
237239
});
238240

239241
let app = Router::new()

crates/openfang-api/tests/load_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async fn start_test_server() -> TestServer {
5656
peer_registry: None,
5757
bridge_manager: tokio::sync::Mutex::new(None),
5858
channels_config: tokio::sync::RwLock::new(Default::default()),
59+
shutdown_notify: Arc::new(tokio::sync::Notify::new()),
5960
});
6061

6162
let app = Router::new()

crates/openfang-cli/src/main.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,23 @@ fn cmd_stop() {
13321332
let client = daemon_client();
13331333
match client.post(format!("{base}/api/shutdown")).send() {
13341334
Ok(r) if r.status().is_success() => {
1335-
ui::success("Daemon is shutting down");
1335+
// Wait for daemon to actually stop (up to 5 seconds)
1336+
for _ in 0..10 {
1337+
std::thread::sleep(std::time::Duration::from_millis(500));
1338+
if find_daemon().is_none() {
1339+
ui::success("Daemon stopped");
1340+
return;
1341+
}
1342+
}
1343+
// Still alive — force kill via PID
1344+
if let Some(home) = dirs::home_dir() {
1345+
let of_dir = home.join(".openfang");
1346+
if let Some(info) = read_daemon_info(&of_dir) {
1347+
force_kill_pid(info.pid);
1348+
let _ = std::fs::remove_file(of_dir.join("daemon.json"));
1349+
}
1350+
}
1351+
ui::success("Daemon stopped (forced)");
13361352
}
13371353
Ok(r) => {
13381354
ui::error(&format!("Shutdown request failed ({})", r.status()));
@@ -1351,6 +1367,21 @@ fn cmd_stop() {
13511367
}
13521368
}
13531369

1370+
fn force_kill_pid(pid: u32) {
1371+
#[cfg(unix)]
1372+
{
1373+
let _ = std::process::Command::new("kill")
1374+
.args(["-9", &pid.to_string()])
1375+
.output();
1376+
}
1377+
#[cfg(windows)]
1378+
{
1379+
let _ = std::process::Command::new("taskkill")
1380+
.args(["/PID", &pid.to_string(), "/F"])
1381+
.output();
1382+
}
1383+
}
1384+
13541385
/// Show context-aware error for kernel boot failures.
13551386
fn boot_kernel_error(e: &openfang_kernel::error::KernelError) {
13561387
let msg = e.to_string();

docker-compose.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
# NOTE: The GHCR image (ghcr.io/rightnow-ai/openfang) is not yet public.
2+
# For now, build from source using `docker compose up --build`.
3+
# See: https://github.com/RightNow-AI/openfang/issues/12
4+
15
version: "3.8"
26
services:
37
openfang:
48
build: .
5-
image: ghcr.io/rightnow-ai/openfang:latest
9+
# image: ghcr.io/rightnow-ai/openfang:latest # Uncomment when GHCR is public
610
ports:
711
- "4200:4200"
812
volumes:

0 commit comments

Comments
 (0)