crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! `cortex serve` — run the MCP stdio JSON-RPC server.
//!
//! Opens the Cortex data store, registers all available tools, prints an
//! ADR 0047 operator-confirmation token to stderr, then blocks on
//! `cortex_mcp::serve::run_stdio_server` until stdin reaches EOF.
//!
//! ## Flags (CR-8)
//!
//! - `--db <PATH>` — explicit SQLite database path. Defaults under
//!   `$XDG_DATA_HOME/cortex/` via `DataLayout::resolve`.
//! - `--event-log <PATH>` — explicit JSONL event-log path. Same default
//!   resolution. Flag names match `CloseArgs` exactly.
//!
//! ## Confirmation token (ADR 0047)
//!
//! A 6-character alphanumeric token is generated from the OS random source
//! at startup and printed to stderr:
//!
//! ```text
//! cortex serve: confirmation token: <TOKEN>
//! ```
//!
//! The token is printed before the stdio loop starts so the operator can
//! record it before handing the process's stdin/stdout to a client.
//!
//! ## Tool registration
//!
//! All 18 MCP tool implementations are registered at startup. Tools that
//! require the store receive an `Arc<Mutex<Pool>>` at construction time.
//! The `CortexSessionCommitTool` additionally receives an
//! `Arc<RwLock<Option<String>>>` carrying the operator confirmation token.

use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};

use clap::Args;
use cortex_mcp::tool_handler::GateId;
use cortex_mcp::tool_registry::ToolRegistry;
use cortex_mcp::tools::admit_axiom::CortexAdmitAxiomTool;
use cortex_mcp::tools::audit_verify::CortexAuditVerifyTool;
use cortex_mcp::tools::config_status::CortexConfigTool;
use cortex_mcp::tools::context::CortexContextTool;
use cortex_mcp::tools::decay_status::CortexDecayStatusTool;
use cortex_mcp::tools::doctor::CortexDoctorTool;
use cortex_mcp::tools::health::CortexMemoryHealthTool;
use cortex_mcp::tools::memory_accept::CortexMemoryAcceptTool;
use cortex_mcp::tools::memory_embed::CortexMemoryEmbedTool;
use cortex_mcp::tools::memory_list::CortexMemoryListTool;
use cortex_mcp::tools::memory_note::CortexMemoryNoteTool;
use cortex_mcp::tools::memory_outcome::CortexMemoryOutcomeTool;
use cortex_mcp::tools::models_list::CortexModelsListTool;
use cortex_mcp::tools::reflect::CortexReflectTool;
use cortex_mcp::tools::search::CortexSearchTool;
use cortex_mcp::tools::session_close::CortexSessionCloseTool;
use cortex_mcp::tools::session_commit::CortexSessionCommitTool;
use cortex_mcp::tools::suggest::CortexSuggestTool;

use crate::config::{AutoCommitSource, McpConfig};
use crate::exit::Exit;
use crate::paths::DataLayout;

/// `cortex serve` arguments.
#[derive(Debug, Args)]
pub struct ServeArgs {
    /// Override the SQLite database path. Defaults under
    /// `$XDG_DATA_HOME/cortex/`.
    #[arg(long, value_name = "PATH")]
    pub db: Option<PathBuf>,

    /// Override the JSONL event-log path. Defaults under
    /// `$XDG_DATA_HOME/cortex/`.
    #[arg(long = "event-log", value_name = "PATH")]
    pub event_log: Option<PathBuf>,
}

/// Run `cortex serve`.
pub fn run(args: ServeArgs) -> Exit {
    // ── 1. Resolve data layout ──────────────────────────────────────────────
    let layout = match DataLayout::resolve(args.db, args.event_log) {
        Ok(l) => l,
        Err(exit) => return exit,
    };

    tracing::debug!(
        db = %layout.db_path.display(),
        event_log = %layout.event_log_path.display(),
        "cortex serve: resolved data layout"
    );

    // ── 2. Open the store pool ──────────────────────────────────────────────
    // The store must exist before the server accepts tool calls that write to
    // it. An absent DB is a PreconditionUnmet — run `cortex init` first.
    if !layout.db_path.exists() {
        eprintln!(
            "cortex serve: precondition unmet: database {} does not exist; run `cortex init` first. no state was changed.",
            layout.db_path.display()
        );
        return Exit::PreconditionUnmet;
    }

    let raw_pool = match cortex_store::Pool::open(&layout.db_path) {
        Ok(p) => p,
        Err(err) => {
            eprintln!("cortex serve: failed to open database: {err}");
            return Exit::Internal;
        }
    };

    if let Err(err) = cortex_store::migrate::apply_pending(&raw_pool) {
        eprintln!("cortex serve: failed to apply migrations: {err}");
        return Exit::Internal;
    }

    // Wrap in Arc<Mutex<_>> so tool constructors can share the connection.
    // rusqlite::Connection is !Sync; the Mutex satisfies the ToolHandler
    // Send + Sync bound while keeping single-threaded access at the call site.
    let pool = Arc::new(Mutex::new(raw_pool));

    // ── 3. Generate and store the ADR 0047 operator-confirmation token ──────
    //
    // Auto-commit can be enabled via CORTEX_MCP_AUTO_COMMIT=1 (env var, highest
    // precedence) or [mcp] auto_commit = true in cortex.toml.  Either path
    // bypasses the ADR 0047 §3 safety guarantee and MUST only be used in
    // operator-controlled contexts where the operator IS the pipeline.  The
    // warning is printed to stderr before the stdio loop starts so it appears
    // in any MCP log pane alongside the token line.
    let mcp_cfg = McpConfig::resolve();
    let auto_commit_enabled = mcp_cfg.auto_commit;
    if auto_commit_enabled {
        let source_label = match mcp_cfg.auto_commit_source {
            AutoCommitSource::EnvVar => "env var CORTEX_MCP_AUTO_COMMIT=1",
            AutoCommitSource::ConfigFile => "[mcp] auto_commit = true in config file",
            AutoCommitSource::NotSet => "unknown source",
        };
        eprintln!(
            "cortex serve: WARNING: session auto-commit is ENABLED (source: {source_label})"
        );
        eprintln!(
            "cortex serve: WARNING: ADR 0047 operator-confirmation token is BYPASSED — \
             every session will self-commit without operator input"
        );
        eprintln!(
            "cortex serve: WARNING: to disable, unset CORTEX_MCP_AUTO_COMMIT or set \
             [mcp] auto_commit = false in cortex.toml"
        );
    }

    let token = confirmation_token();
    eprintln!("cortex serve: confirmation token: {token}");

    // The token is placed behind Arc<RwLock<Option<String>>> so that
    // CortexSessionCommitTool can read it on every call without additional
    // cloning. It is set to Some before the stdio loop starts.
    let session_token: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(Some(token)));

    // ── 4. Resolve the replay-fixtures directory for session_close ──────────
    // Resolution order: $CORTEX_FIXTURES_DIR env var → data_dir/replay.
    let fixtures_dir: PathBuf = if let Some(p) = std::env::var_os("CORTEX_FIXTURES_DIR") {
        PathBuf::from(p)
    } else {
        layout.data_dir.join("replay")
    };

    let event_log = layout.event_log_path.clone();

    // ── 5. Build the tool registry ──────────────────────────────────────────
    let _available_gates: &[GateId] = &[];
    let mut registry = ToolRegistry::new();

    // Session tier (no confirmation needed)
    registry.register(Box::new(CortexSearchTool {
        pool: Arc::clone(&pool),
    }));
    registry.register(Box::new(CortexContextTool {
        pool: Arc::clone(&pool),
    }));
    registry.register(Box::new(CortexMemoryHealthTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexConfigTool::new()));
    registry.register(Box::new(CortexSuggestTool::new(Arc::clone(&pool))));

    // Supervised tier (no confirmation token; prominent stderr log)
    registry.register(Box::new(CortexMemoryListTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexMemoryOutcomeTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexDecayStatusTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexDoctorTool::new(
        Arc::clone(&pool),
        event_log.clone(),
    )));
    registry.register(Box::new(CortexAuditVerifyTool::new(
        Arc::clone(&pool),
        event_log.clone(),
    )));
    registry.register(Box::new(CortexReflectTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexModelsListTool::new()));
    registry.register(Box::new(CortexMemoryEmbedTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexMemoryNoteTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexSessionCloseTool::new(
        Arc::clone(&pool),
        event_log.clone(),
        fixtures_dir,
    )));

    // Confirmed tier
    registry.register(Box::new(CortexMemoryAcceptTool::new(
        Arc::clone(&pool),
        Arc::clone(&session_token),
        auto_commit_enabled,
    )));
    registry.register(Box::new(CortexAdmitAxiomTool::new(Arc::clone(&pool))));
    registry.register(Box::new(CortexSessionCommitTool::new(
        Arc::clone(&pool),
        Arc::clone(&session_token),
        auto_commit_enabled,
    )));

    tracing::info!(
        tools = "cortex_search,cortex_context,cortex_memory_health,cortex_config,\
                 cortex_suggest,cortex_memory_list,cortex_memory_outcome,\
                 cortex_decay_status,cortex_doctor,cortex_audit_verify,cortex_reflect,\
                 cortex_models_list,cortex_memory_embed,cortex_memory_note,\
                 cortex_session_close,cortex_memory_accept,cortex_admit_axiom,\
                 cortex_session_commit",
        "cortex serve: starting stdio MCP server"
    );

    // ── 6. Block on the stdio loop ──────────────────────────────────────────
    // The pool Arc remains alive for the duration of the blocking loop so that
    // tool calls issued by the client can acquire the Mutex. It is dropped
    // naturally when run() returns.
    match cortex_mcp::serve::run_stdio_server(registry) {
        Ok(()) => {
            tracing::info!("cortex serve: stdio loop exited cleanly (EOF)");
            Exit::Ok
        }
        Err(err) => {
            eprintln!("cortex serve: stdio server error: {err}");
            Exit::Internal
        }
    }
}

/// Generate a 6-character alphanumeric confirmation token using the OS
/// random source.
///
/// Characters are drawn from `[A-Z0-9]` (36 possibilities per char).
/// Entropy: log2(36^6) ≈ 31 bits — sufficient for a human-confirmation
/// code that must be re-entered in a shell before a gated write is
/// authorised (ADR 0047 operator-confirmation mechanism).
fn confirmation_token() -> String {
    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    const TOKEN_LEN: usize = 6;

    // Use platform random via std's DefaultHasher seed trick is not
    // cryptographic enough; use std::time + pid as a low-quality seed and
    // accept that the token is human-confirmation quality only, not a
    // security secret.
    //
    // For a production-quality implementation use `rand` or `getrandom`
    // when those crates are added as workspace dependencies.
    let seed = {
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .subsec_nanos();
        let pid = std::process::id();
        (nanos as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15)
            ^ (pid as u64).wrapping_mul(0x6c62_272e_07bb_0142)
    };

    let mut state = seed;
    let mut chars = [0u8; TOKEN_LEN];
    for ch in &mut chars {
        // xorshift64
        state ^= state << 13;
        state ^= state >> 7;
        state ^= state << 17;
        *ch = ALPHABET[(state as usize) % ALPHABET.len()];
    }

    String::from_utf8(chars.to_vec()).expect("ALPHABET contains only ASCII")
}