crtx 0.1.0

CLI for the Cortex supervisory memory substrate.
//! Shared helper for the operator-key temporal authority current-use
//! revalidation surfaces (`migrate v2`, `restore apply --production`,
//! `restore recover-apply`, `run --trusted-history`, `audit verify --signed`).
//!
//! Doctrine: ADR 0023 (key lifecycle), ADR 0019 (trust tiering),
//! ADR 0026 (policy lattice), ADR 0036 (proof closure), ADR 0037
//! (runtime truth ceilings). See
//! `docs/design/PHASE_2_6_temporal_authority_revalidation_audit.md`
//! for the audit that scoped this helper.
//!
//! Reference implementation: `cortex principle promote`
//! (`require_operator_temporal_authority` in `principle_promote.rs`).
//! That surface is the only one in tree today that derives the
//! `*_operator_temporal_*` contributor from the durable
//! `authority_key_timeline` rather than from a hard-coded `Allow`
//! literal; this helper lets every other consuming surface adopt the
//! same shape.

use chrono::{DateTime, Utc};
use cortex_core::{PolicyContribution, PolicyOutcome, TemporalAuthorityReport, TrustTier};
use cortex_store::repo::{AuthorityRepo, TemporalAuthorityQuery};
use cortex_store::Pool;

/// Outcome of a temporal-authority revalidation call against the durable
/// key timeline. Surfaces consume either [`Self::contribution`] (to fold
/// the `policy_decision()` outcome into a composed ADR 0026 decision) or
/// [`Self::fail_closed_invariant`] (to surface the stable invariant string
/// to the operator without losing the policy contributor).
#[derive(Debug)]
pub struct TemporalAuthorityContribution {
    /// Revalidation report. `policy_decision().final_outcome` maps:
    /// - `Allow` when the key + principal pass at event time AND now.
    /// - `Quarantine` when valid at event time but invalid now.
    /// - `Reject` when invalid at event time.
    pub report: TemporalAuthorityReport,
    /// The rule id this contribution was emitted under. Mirrors the
    /// `*_operator_temporal_authority` / `*_operator_temporal_use`
    /// contributor names the per-surface composer expects.
    pub rule_id: &'static str,
}

impl TemporalAuthorityContribution {
    /// Final policy outcome to fold into the per-surface composed
    /// decision. `Allow` only when the key + principal pass at event
    /// time AND now.
    #[must_use]
    pub fn outcome(&self) -> PolicyOutcome {
        self.report.policy_decision().final_outcome
    }

    /// Human-readable reason. On the failure path this includes the
    /// per-reason wire strings (`signed_after_revocation`,
    /// `revoked_after_signing`, `key_unknown`, etc.) so the operator
    /// transcript names the failing edge.
    #[must_use]
    pub fn reason(&self) -> String {
        if self.report.valid_now {
            return format!(
                "operator temporal authority valid for current use (key {})",
                self.report.key_id,
            );
        }
        let reasons = self
            .report
            .reasons
            .iter()
            .map(|reason| reason.wire_str())
            .collect::<Vec<_>>()
            .join(",");
        if self.report.valid_at_event_time {
            format!(
                "operator temporal authority is historical evidence only (key {}; reasons: {reasons})",
                self.report.key_id,
            )
        } else {
            format!(
                "operator temporal authority invalid at event time (key {}; reasons: {reasons})",
                self.report.key_id,
            )
        }
    }

    /// Build an ADR 0026 [`PolicyContribution`] from this revalidation.
    ///
    /// # Panics
    ///
    /// Static rule ids and reasons are non-empty, so the construction
    /// is infallible in practice. The internal `expect` documents that
    /// invariant.
    #[must_use]
    pub fn contribution(&self) -> PolicyContribution {
        PolicyContribution::new(self.rule_id, self.outcome(), self.reason())
            .expect("temporal authority contribution shape is valid")
    }
}

/// Errors raised while reading the durable key timeline. These are
/// distinct from "revalidation failed because the timeline says the key
/// is revoked" — that case is reported via [`TemporalAuthorityContribution::report`]
/// with `valid_now = false`. Errors here mean we could not load the
/// timeline at all.
#[derive(Debug)]
pub enum TemporalAuthorityError {
    /// SQLite-side read error against `authority_key_timeline` or
    /// `authority_principal_timeline`.
    Store(cortex_store::StoreError),
}

impl std::fmt::Display for TemporalAuthorityError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Store(err) => write!(f, "authority repo read failed: {err}"),
        }
    }
}

impl std::error::Error for TemporalAuthorityError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Store(err) => Some(err),
        }
    }
}

impl From<cortex_store::StoreError> for TemporalAuthorityError {
    fn from(err: cortex_store::StoreError) -> Self {
        Self::Store(err)
    }
}

/// Revalidate operator temporal authority for `key_id` at `event_time`.
///
/// The `minimum_trust_tier` argument carries the per-surface doctrine
/// choice from `PHASE_2_6_temporal_authority_revalidation_audit.md` §6.2:
///
/// - `cortex migrate v2`, `cortex restore apply --production`,
///   `cortex restore recover-apply` -> [`TrustTier::Operator`]
///   (destructive doctrine roots; ADR 0026 §4 hard wall).
/// - `cortex run --trusted-history` -> [`TrustTier::Verified`]
///   (ADR 0035 / 0037 sub-operator runtime).
/// - `cortex audit verify --signed` -> [`TrustTier::Verified`]
///   (chain inherits the chain's minimum tier).
pub fn revalidate_operator_temporal_authority(
    pool: &Pool,
    rule_id: &'static str,
    key_id: &str,
    event_time: DateTime<Utc>,
    minimum_trust_tier: TrustTier,
) -> Result<TemporalAuthorityContribution, TemporalAuthorityError> {
    let now = Utc::now();
    let report = AuthorityRepo::new(pool).revalidate(&TemporalAuthorityQuery {
        key_id: key_id.to_string(),
        event_time,
        now,
        minimum_trust_tier,
    })?;
    Ok(TemporalAuthorityContribution { report, rule_id })
}

/// Build the stable invariant token a surface emits on the failure
/// path. The invariants below match the names cataloged in the audit
/// document §"Tests / Stable invariants":
///
/// - `migrate.v2.operator_temporal_authority.revalidation_failed`
/// - `restore.production.operator_temporal_authority.revalidation_failed`
/// - `restore.recover_apply.operator_temporal_authority.revalidation_failed`
/// - `run.trusted_history.operator_temporal_authority.revalidation_failed`
/// - `audit.verify.operator_temporal_authority.revalidation_failed`
#[must_use]
pub fn revalidation_failed_invariant(surface: &str) -> String {
    format!("{surface}.operator_temporal_authority.revalidation_failed")
}