/// Engram Server — HTTP API for Engram with sync and swarm activation. /// /// # Endpoints /// /// ## Core /// GET /stats — node/edge counts /// POST /nodes — create a node /// GET /nodes/{id} — get a node /// POST /edges — create an edge /// GET /nodes/{id}/edges — list edges from a node /// POST /activate — spreading activation /// POST /search — embedding search /// POST /decay — apply salience decay /// POST /consolidate — promote Episodic → Semantic /// /// ## Sync (auth required) /// GET /sync/delta?since={ms}&peer_id={uuid} — generate delta /// POST /sync/push — receive incoming delta /// POST /sync/peers — register peer /// GET /sync/peers — list peers /// DELETE /sync/peers/{id} — remove peer /// /// ## Swarm /// POST /swarm/activate — distributed activation (auth required) /// GET /swarm/status — peer health (auth required) use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; use axum::{ middleware, routing::{delete, get, post}, Router, }; use engram_core::EngramDb; use engram_sync::{SyncConfig, SyncEngine}; use tokio::time::interval; use tower_http::cors::CorsLayer; use tracing::info; use tracing_subscriber::EnvFilter; mod auth; mod routes; mod state; use auth::require_auth; use state::AppState; #[tokio::main] async fn main() -> anyhow::Result<()> { // Logging tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), ) .init(); // Configuration from environment (with sensible defaults) let db_path = std::env::var("ENGRAM_DB_PATH").unwrap_or_else(|_| "./engram-data".to_string()); let bind_addr = std::env::var("ENGRAM_BIND").unwrap_or_else(|_| "0.0.0.0:8742".to_string()); let api_key = std::env::var("ENGRAM_API_KEY").unwrap_or_else(|_| { let key = uuid::Uuid::new_v4().to_string(); eprintln!("No ENGRAM_API_KEY set — generated key: {}", key); key }); // Open database let db = EngramDb::open(&PathBuf::from(&db_path))?; let db = Arc::new(Mutex::new(db)); info!("Database opened at {}", db_path); // Sync engine — wrapped in tokio::sync::Mutex so it can be held across .await let sync_config = SyncConfig { our_id: uuid::Uuid::new_v4(), our_name: std::env::var("ENGRAM_PEER_NAME").unwrap_or_else(|_| "engram-local".to_string()), api_key: api_key.clone(), default_sync_tiers: vec![engram_core::types::MemoryTier::Semantic], sync_interval_secs: std::env::var("ENGRAM_SYNC_INTERVAL_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(300), }; let sync_interval_secs = sync_config.sync_interval_secs; let sync_engine = Arc::new(tokio::sync::Mutex::new(SyncEngine::new(db.clone(), sync_config))); { let e = sync_engine.lock().await; info!( peer_name = e.our_name(), peer_id = %e.our_id(), sync_interval_secs, "Sync engine ready" ); } // Background sync task — tokio::sync::Mutex guard is Send-safe { let engine_arc = sync_engine.clone(); tokio::spawn(async move { let mut ticker = interval(Duration::from_secs(sync_interval_secs)); loop { ticker.tick().await; let report = { let mut e = engine_arc.lock().await; e.sync_all().await }; if report.peers_synced > 0 || !report.errors.is_empty() { info!( peers_synced = report.peers_synced, nodes_received = report.nodes_received, nodes_sent = report.nodes_sent, errors = report.errors.len(), "Sync cycle complete" ); } } }); } // Shared state let state = Arc::new(AppState { db: db.clone(), sync_engine: sync_engine.clone(), api_key: api_key.clone(), }); // Protected sync/swarm routes (auth middleware applied) let sync_routes = Router::new() .route("/sync/delta", get(routes::sync::get_delta)) .route("/sync/push", post(routes::sync::push_delta)) .route("/sync/peers", get(routes::sync::list_peers)) .route("/sync/peers", post(routes::sync::register_peer)) .route("/sync/peers/{id}", delete(routes::sync::delete_peer)) .route("/swarm/activate", post(routes::swarm::swarm_activate)) .route("/swarm/status", get(routes::swarm::swarm_status)) .layer(middleware::from_fn_with_state( state.clone(), require_auth, )); // Open core routes (no auth) let core_routes = Router::new() .route("/stats", get(routes::core::get_stats)) .route("/nodes", post(routes::core::create_node)) .route("/nodes/{id}", get(routes::core::get_node)) .route("/edges", post(routes::core::create_edge)) .route("/nodes/{id}/edges", get(routes::core::get_edges_from)) .route("/activate", post(routes::core::activate)) .route("/search", post(routes::core::search_embedding)) .route("/decay", post(routes::core::decay)) .route("/consolidate", post(routes::core::consolidate)); let app = Router::new() .merge(core_routes) .merge(sync_routes) .layer(CorsLayer::permissive()) .with_state(state); let listener = tokio::net::TcpListener::bind(&bind_addr).await?; info!("Engram server listening on {}", bind_addr); axum::serve(listener, app).await?; Ok(()) }