1use std::sync::Arc;
8
9use ijima_core::{IjimaError, Result, Store};
10
11use crate::IjimaAuth;
12use crate::api;
13use crate::key_store;
14
15#[derive(Debug, Clone)]
17pub struct DaemonConfig {
18 pub host: String,
20 pub port: u16,
22}
23
24pub 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 let file = crate::config::load().unwrap_or_default();
39 Self {
40 host: crate::config::resolve_str("IJIMA_HOST", file.host, "127.0.0.1"),
41 port: std::env::var("IJIMA_PORT")
42 .ok()
43 .and_then(|p| p.parse().ok())
44 .or(file.port)
45 .unwrap_or(7373),
46 }
47 }
48}
49
50#[cfg(feature = "federation")]
71fn federation_config_from_env() -> ijima_core::federation::InstanceFederationConfig {
72 use ijima_core::federation::{
73 AuthoritativeScope, InstanceFederationConfig, InstanceId, InstanceRole,
74 };
75 use std::str::FromStr;
76 let instance_id = std::env::var("IJIMA_INSTANCE_ID")
77 .map(InstanceId::new)
78 .unwrap_or_default();
79 let role = std::env::var("IJIMA_INSTANCE_ROLE")
80 .ok()
81 .and_then(|r| InstanceRole::from_str(&r).ok())
82 .unwrap_or(InstanceRole::Unifying);
83 let authoritative_scopes = std::env::var("IJIMA_INSTANCE_SCOPES")
84 .ok()
85 .map(|s| {
86 s.split(',')
87 .filter_map(|p| AuthoritativeScope::from_str(p.trim()).ok())
88 .collect::<Vec<_>>()
89 })
90 .filter(|v| !v.is_empty())
91 .unwrap_or_else(|| vec![AuthoritativeScope::new("local", "*")]);
92 let capability_policy_ref = std::env::var("IJIMA_CAPABILITY_POLICY_REF")
93 .ok()
94 .filter(|s| !s.is_empty());
95 InstanceFederationConfig {
96 instance_id,
97 role,
98 authoritative_scopes,
99 outbound_links: Vec::new(),
100 capability_policy_ref,
101 }
102}
103
104pub async fn serve(config: &DaemonConfig) -> Result<()> {
105 init_tracing();
106 let file_config = crate::config::load()?;
109 let key_path = key_store::default_key_path()?;
110 let seed = key_store::load_or_create(&key_path)?;
111 let auth = Arc::new(IjimaAuth::from_embedded_policy_with_seed(seed)?);
112
113 #[cfg(feature = "embeddings-candle")]
114 let embedder: Option<Arc<dyn ijima_core::Embedder>> = {
115 let model = crate::config::resolve_str(
117 "IJIMA_EMBED_MODEL",
118 file_config.embedding_model.clone(),
119 crate::embeddings_candle::DEFAULT_MODEL,
120 );
121 let revision = std::env::var("IJIMA_EMBED_REVISION").unwrap_or_else(|_| "main".into());
122 let e: Arc<dyn ijima_core::Embedder> = Arc::new(
123 crate::embeddings_candle::CandleEmbedder::from_hub_model(&model, &revision)?,
124 );
125 tracing::info!(model = %e.model_id(), "embedder loaded");
126 Some(e)
127 };
128 #[cfg(not(feature = "embeddings-candle"))]
129 let embedder: Option<Arc<dyn ijima_core::Embedder>> = None;
130
131 let data_dir = crate::config::resolve_data_dir()?;
134 let db_path = data_dir.join("ijima.db");
135
136 #[cfg(feature = "embeddings-candle")]
137 let store_inner = Arc::new(
138 crate::SurrealStore::open_persistent_with(&db_path, embedder.clone().unwrap()).await?,
139 );
140 #[cfg(not(feature = "embeddings-candle"))]
141 let store_inner = Arc::new(crate::SurrealStore::open_persistent(&db_path).await?);
142 let store: Arc<dyn Store> = store_inner.clone();
143 let kg: Arc<dyn ijima_core::KnowledgeGraph> = store_inner;
144
145 let revocations = store.list_revocations().await?;
148 if !revocations.is_empty() {
149 tracing::info!(count = revocations.len(), "hydrated token revocations");
150 }
151 auth.hydrate_revocations(&revocations);
152
153 #[cfg(feature = "rate-limit")]
157 let rate_limiter: Option<crate::rate_limit::RateLimitState> =
158 if std::env::var_os("IJIMA_RATE_DISABLE").is_some() {
159 None
160 } else {
161 let base = crate::config::resolve_f64("IJIMA_RATE_BASE", file_config.rate_base, 10.0);
162 let mult = crate::config::resolve_f64(
163 "IJIMA_RATE_MULTIPLIER",
164 file_config.rate_multiplier,
165 1.0,
166 );
167 tracing::info!(
168 base_tokens_per_second = base,
169 multiplier = mult,
170 "rate limiting enabled (Schubert intersection-number capacity)"
171 );
172 Some(crate::rate_limit::make_rate_limiter(base, mult))
173 };
174
175 let app = api::app(
176 auth,
177 store,
178 kg,
179 embedder,
180 std::sync::Arc::new(crate::redaction::Redactor::new()),
181 #[cfg(feature = "rate-limit")]
182 rate_limiter,
183 #[cfg(feature = "federation")]
184 std::sync::Arc::new(federation_config_from_env()),
185 );
186
187 let addr = format!("{}:{}", config.host, config.port);
188
189 #[cfg(feature = "tls")]
192 if let (Some(cert_path), Some(key_path)) = (
193 std::env::var_os("IJIMA_TLS_CERT"),
194 std::env::var_os("IJIMA_TLS_KEY"),
195 ) {
196 let tls_config =
197 axum_server::tls_rustls::RustlsConfig::from_pem_file(&cert_path, &key_path)
198 .await
199 .map_err(|e| IjimaError::Store {
200 detail: format!("tls config: {e}"),
201 })?;
202 let socket_addr: std::net::SocketAddr = addr.parse().map_err(|e| IjimaError::Store {
203 detail: format!("parse {addr}: {e}"),
204 })?;
205 let listener = axum_server::bind_rustls(socket_addr, tls_config);
206 eprintln!("ijima: listening on https://{addr}");
207 listener
208 .serve(app.into_make_service())
209 .await
210 .map_err(|e| IjimaError::Store {
211 detail: format!("serve: {e}"),
212 })?;
213 return Ok(());
214 }
215
216 let listener = tokio::net::TcpListener::bind(&addr)
217 .await
218 .map_err(|e| IjimaError::Store {
219 detail: format!("bind {addr}: {e}"),
220 })?;
221 tracing::info!(addr = %addr, "ijima listening");
222 axum::serve(listener, app)
223 .await
224 .map_err(|e| IjimaError::Store {
225 detail: format!("serve: {e}"),
226 })?;
227 Ok(())
228}