This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/engram/crates/engram-server/src/main.rs
T

166 lines
5.8 KiB
Rust

/// 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(())
}