#![recursion_limit = "512"]
use ai_memory::daemon_runtime::Cli;
use ai_memory::{audit, color, config, daemon_runtime, logging, permissions};
use anyhow::Result;
use clap::Parser;
#[cfg(test)]
use ai_memory::cli::helpers::{human_age, id_short};
#[cfg(test)]
use ai_memory::tls;
#[tokio::main]
async fn main() -> Result<()> {
color::init();
let app_config = config::AppConfig::load();
config::AppConfig::write_default_if_missing();
daemon_runtime::apply_anonymize_default(&app_config);
config::set_active_permissions_mode(app_config.effective_permissions_mode());
let resolved_hmac_secret = app_config.effective_hooks_hmac_secret();
if let Err(msg) =
ai_memory::subscriptions::validate_hmac_secret_hex(resolved_hmac_secret.as_deref())
{
eprintln!("ai-memory: boot refused — #1048 invalid hmac_secret\n {msg}");
std::process::exit(78); }
config::set_active_hooks_hmac_secret(resolved_hmac_secret);
config::set_allow_loopback_webhooks(app_config.effective_allow_loopback_webhooks());
permissions::set_active_permission_rules(app_config.effective_permission_rules());
let _log_guard =
logging::init_file_logging(&app_config.effective_logging()).unwrap_or_else(|e| {
eprintln!("ai-memory: file logging init failed (continuing without): {e}");
None
});
if let Err(e) = audit::init_from_config(&app_config.effective_audit()) {
eprintln!("ai-memory: audit init failed (continuing without): {e}");
}
init_forensic_audit(&app_config);
let cli = Cli::parse();
daemon_runtime::run(cli, &app_config).await
}
fn init_forensic_audit(app_config: &config::AppConfig) {
let audit_cfg = app_config.effective_audit();
let log_path = ai_memory::audit::resolve_audit_path(&audit_cfg);
let Some(dir) = log_path.parent() else {
eprintln!("ai-memory: forensic init skipped (could not resolve audit dir)");
return;
};
let agent_id = ai_memory::identity::resolve_agent_id(None, None)
.unwrap_or_else(|_| "ai-memory".to_string());
let signing_key =
ai_memory::governance::audit::load_daemon_signing_key(&agent_id).unwrap_or(None);
if let Err(e) = ai_memory::governance::audit::init(dir, signing_key) {
eprintln!("ai-memory: forensic audit init failed (continuing unsigned): {e}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_short_truncates() {
assert_eq!(id_short("abcdefghijklmnop"), "abcdefgh");
}
#[test]
fn id_short_short_input() {
assert_eq!(id_short("abc"), "abc");
}
#[test]
fn id_short_empty() {
assert_eq!(id_short(""), "");
}
#[test]
fn human_age_just_now() {
let now = chrono::Utc::now().to_rfc3339();
assert_eq!(human_age(&now), "just now");
}
#[test]
fn human_age_minutes() {
let past = (chrono::Utc::now() - chrono::Duration::minutes(5)).to_rfc3339();
let age = human_age(&past);
assert!(age.contains("m ago"), "got: {age}");
}
#[test]
fn human_age_hours() {
let past = (chrono::Utc::now() - chrono::Duration::hours(3)).to_rfc3339();
let age = human_age(&past);
assert!(age.contains("h ago"), "got: {age}");
}
#[test]
fn human_age_days() {
let past = (chrono::Utc::now() - chrono::Duration::days(5)).to_rfc3339();
let age = human_age(&past);
assert!(age.contains("d ago"), "got: {age}");
}
#[test]
fn human_age_invalid_returns_input() {
assert_eq!(human_age("not-a-date"), "not-a-date");
}
#[test]
fn auto_namespace_returns_nonempty() {
let ns = ai_memory::cli::helpers::auto_namespace();
assert!(!ns.is_empty());
}
#[tokio::test]
async fn fingerprint_allowlist_tolerates_trailing_comments() {
let fp_a = "a".repeat(64);
let fp_b = "b".repeat(64);
let fp_c = format!("{}:{}", "c".repeat(32), "c".repeat(32));
let body = format!(
"# authorised mTLS peers\n\
{fp_a} # node-1\n\
\n\
sha256:{fp_b}\t# node-2 with tab\n\
{fp_c}\n"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), body).unwrap();
let set = tls::load_fingerprint_allowlist(tmp.path()).await.unwrap();
assert_eq!(set.len(), 3, "expected 3 fingerprints, got {}", set.len());
assert!(set.contains(&[0xaa; 32]));
assert!(set.contains(&[0xbb; 32]));
assert!(set.contains(&[0xcc; 32]));
}
#[test]
fn init_forensic_audit_with_temp_dir_does_not_panic() {
let root = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.join(".local-runs")
.join("main-init-forensic-audit");
std::fs::create_dir_all(&root).ok();
let tmp = tempfile::tempdir_in(&root).expect("tempdir under .local-runs");
let prev = std::env::var("AI_MEMORY_AUDIT_DIR").ok();
unsafe { std::env::set_var("AI_MEMORY_AUDIT_DIR", tmp.path()) };
let app_config = config::AppConfig::default();
init_forensic_audit(&app_config);
match prev {
Some(v) => unsafe { std::env::set_var("AI_MEMORY_AUDIT_DIR", v) },
None => unsafe { std::env::remove_var("AI_MEMORY_AUDIT_DIR") },
}
}
#[tokio::test]
async fn fingerprint_allowlist_rejects_embedded_whitespace() {
let body = format!("{} {}\n", "a".repeat(32), "a".repeat(32));
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), body).unwrap();
let err = tls::load_fingerprint_allowlist(tmp.path())
.await
.unwrap_err();
assert!(
err.to_string().contains("unexpected character"),
"expected strict char-set error, got: {err}"
);
}
}