Skip to main content

ijima_server/
server.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! Daemon wiring: construct the auth core + store, build the router,
5//! and bind/listen.
6
7use std::sync::Arc;
8
9use ijima_core::{IjimaError, Result, Store};
10
11use crate::IjimaAuth;
12use crate::api;
13use crate::key_store;
14
15/// Daemon configuration, resolved from env vars or CLI args.
16#[derive(Debug, Clone)]
17pub struct DaemonConfig {
18    /// Bind host. Default `127.0.0.1` (`IJIMA_HOST`).
19    pub host: String,
20    /// Bind port. Default `7373` (`IJIMA_PORT`).
21    pub port: u16,
22}
23
24/// Initializes structured logging (`tracing`) if not already installed.
25/// Idempotent — safe to call from both the daemon and CLI paths.
26pub fn init_tracing() {
27    use tracing_subscriber::EnvFilter;
28    let _ = tracing_subscriber::fmt()
29        .with_env_filter(
30            EnvFilter::try_from_env("IJIMA_LOG").unwrap_or_else(|_| EnvFilter::new("ijima=info")),
31        )
32        .try_init();
33}
34
35impl Default for DaemonConfig {
36    fn default() -> Self {
37        Self {
38            host: std::env::var("IJIMA_HOST").unwrap_or_else(|_| "127.0.0.1".into()),
39            port: std::env::var("IJIMA_PORT")
40                .ok()
41                .and_then(|p| p.parse().ok())
42                .unwrap_or(7373),
43        }
44    }
45}
46
47/// Runs the Ijima HTTP daemon: loads the issuer key (creating it on first
48/// run), opens the embedded SurrealDB store, builds the router, and
49/// serves until interrupted.
50///
51/// # Errors
52///
53/// Returns [`IjimaError`] if the key, store, or socket cannot be opened.
54pub async fn serve(config: &DaemonConfig) -> Result<()> {
55    init_tracing();
56    let key_path = key_store::default_key_path()?;
57    let seed = key_store::load_or_create(&key_path)?;
58    let auth = Arc::new(IjimaAuth::from_embedded_policy_with_seed(seed)?);
59
60    #[cfg(feature = "embeddings-candle")]
61    let embedder: Option<Arc<dyn ijima_core::Embedder>> = {
62        let e: Arc<dyn ijima_core::Embedder> =
63            Arc::new(crate::embeddings_candle::CandleEmbedder::from_env()?);
64        tracing::info!(model = %e.model_id(), "embedder loaded");
65        Some(e)
66    };
67    #[cfg(not(feature = "embeddings-candle"))]
68    let embedder: Option<Arc<dyn ijima_core::Embedder>> = None;
69
70    // Persistent on disk by default (SurrealKv). The data dir is
71    // $IJIMA_DIR (default ~/.ijima); the store lives at ijima.db.
72    let data_dir = std::env::var("IJIMA_DIR")
73        .map(std::path::PathBuf::from)
74        .or_else(|_| {
75            std::env::var_os("HOME")
76                .map(|h| std::path::PathBuf::from(h).join(".ijima"))
77                .ok_or_else(|| {
78                    ijima_core::IjimaError::invalid_input(
79                        "cannot resolve data dir: set IJIMA_DIR or HOME",
80                    )
81                })
82        })?;
83    let db_path = data_dir.join("ijima.db");
84
85    #[cfg(feature = "embeddings-candle")]
86    let store_inner = Arc::new(
87        crate::SurrealStore::open_persistent_with(&db_path, embedder.clone().unwrap()).await?,
88    );
89    #[cfg(not(feature = "embeddings-candle"))]
90    let store_inner = Arc::new(crate::SurrealStore::open_persistent(&db_path).await?);
91    let store: Arc<dyn Store> = store_inner.clone();
92    let kg: Arc<dyn ijima_core::KnowledgeGraph> = store_inner;
93
94    // Schubert geometric rate limiting (Phase 3.4). Configurable via env;
95    // disabled when IJIMA_RATE_DISABLE is set (tests, CI). Capacity scales
96    // with the capability's Schubert intersection number (codimension).
97    #[cfg(feature = "rate-limit")]
98    let rate_limiter: Option<crate::rate_limit::RateLimitState> =
99        if std::env::var_os("IJIMA_RATE_DISABLE").is_some() {
100            None
101        } else {
102            let base = std::env::var("IJIMA_RATE_BASE")
103                .ok()
104                .and_then(|v| v.parse().ok())
105                .unwrap_or(10.0);
106            let mult = std::env::var("IJIMA_RATE_MULTIPLIER")
107                .ok()
108                .and_then(|v| v.parse().ok())
109                .unwrap_or(1.0);
110            tracing::info!(
111                base_tokens_per_second = base,
112                multiplier = mult,
113                "rate limiting enabled (Schubert intersection-number capacity)"
114            );
115            Some(crate::rate_limit::make_rate_limiter(base, mult))
116        };
117
118    let app = api::app(
119        auth,
120        store,
121        kg,
122        embedder,
123        std::sync::Arc::new(crate::redaction::Redactor::new()),
124        #[cfg(feature = "rate-limit")]
125        rate_limiter,
126    );
127
128    let addr = format!("{}:{}", config.host, config.port);
129
130    // TLS: if both cert and key env vars are set, serve over HTTPS.
131    // Plain HTTP is the default — no config = no TLS.
132    #[cfg(feature = "tls")]
133    if let (Some(cert_path), Some(key_path)) = (
134        std::env::var_os("IJIMA_TLS_CERT"),
135        std::env::var_os("IJIMA_TLS_KEY"),
136    ) {
137        let tls_config =
138            axum_server::tls_rustls::RustlsConfig::from_pem_file(&cert_path, &key_path)
139                .await
140                .map_err(|e| IjimaError::Store {
141                    detail: format!("tls config: {e}"),
142                })?;
143        let listener = axum_server::bind_rustls(
144            addr.parse().map_err(|e| IjimaError::Store {
145                detail: format!("parse {addr}: {e}"),
146            })?,
147            tls_config,
148        );
149        eprintln!("ijima: listening on https://{addr}");
150        listener
151            .serve(app.into_make_service())
152            .await
153            .map_err(|e| IjimaError::Store {
154                detail: format!("serve: {e}"),
155            })?;
156        return Ok(());
157    }
158
159    let listener = tokio::net::TcpListener::bind(&addr)
160        .await
161        .map_err(|e| IjimaError::Store {
162            detail: format!("bind {addr}: {e}"),
163        })?;
164    tracing::info!(addr = %addr, "ijima listening");
165    axum::serve(listener, app)
166        .await
167        .map_err(|e| IjimaError::Store {
168            detail: format!("serve: {e}"),
169        })?;
170    Ok(())
171}