cordance-cli 0.1.1

Cordance CLI — installs the `cordance` binary. The umbrella package `cordance` re-exports this entry; either install command works.
Documentation
//! Cortex tier (authority-bearing): `cordance_cortex_receipt`.
//!
//! Generates a `cordance-cortex-receipt-v1-candidate` for the current
//! target, writes it to `.cordance/cortex-receipt.json`, and returns the
//! receipt inline so the caller does not need filesystem access.
//!
//! Doctrine invariant: every authority flag inside `authority_boundary`
//! must be `false` (except `candidate_only`). `AuthorityBoundary::candidate_only()`
//! enforces this constructor-side; we re-assert it in the tool body so a
//! future refactor of the receipt builder cannot accidentally widen the
//! boundary without being caught here.

use camino::Utf8PathBuf;
use cordance_core::receipt::CortexReceiptV1Candidate;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::config::Config;
use crate::mcp::error::{McpToolError, McpToolResult};

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct CortexReceiptParams {
    #[serde(default)]
    pub target: Option<String>,
    /// When `true`, the receipt is *not* written to disk; only returned
    /// inline. Useful for clients that have no filesystem access at all.
    #[serde(default)]
    pub dry_run: bool,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct CortexReceiptOutput {
    pub schema: String,
    /// Always `.cordance/cortex-receipt.json` under `target`; absent when
    /// `dry_run = true`.
    #[schemars(with = "Option<String>")]
    pub written_path: Option<Utf8PathBuf>,
    /// The receipt itself. Schema literal:
    /// `cordance-cortex-receipt-v1-candidate`. We do not derive `JsonSchema`
    /// on the cordance-core struct because its shape mirrors the axiom
    /// receipt contract; clients validate against that contract instead.
    #[schemars(with = "serde_json::Value")]
    pub receipt: CortexReceiptV1Candidate,
}

pub fn receipt(
    target: &Utf8PathBuf,
    cfg: &Config,
    dry_run: bool,
) -> McpToolResult<CortexReceiptOutput> {
    let pack = super::pack::build_pack(target, cfg)?;
    let receipt = cordance_cortex::build_receipt(&pack)
        .map_err(|e| McpToolError::internal_redacted("receipt build", e))?;

    // Defence in depth: this struct is the only authority claim Cordance can
    // emit. Even if the builder is later refactored, the MCP path must
    // continue to refuse any boundary widening.
    assert_authority_invariants(&receipt)?;

    let written_path = if dry_run {
        None
    } else {
        // Round-6 redteam #2 CRITICAL: round-5's symlink-write hardening
        // covered the CLI cortex_cmd path but missed THIS MCP cortex site —
        // raw `std::fs::write` here would follow a target-planted symlink at
        // `<target>/.cordance/cortex-receipt.json` and write through to
        // operator-owned files. Route through `cordance_core::fs::
        // safe_write_with_mkdir` so the reparse-point check fires for both
        // the parent dir and the receipt file itself.
        let out_path = target.join(".cordance").join("cortex-receipt.json");
        let json = serde_json::to_string_pretty(&receipt)
            .map_err(|e| McpToolError::internal_redacted("receipt serialise", e))?;
        cordance_core::fs::safe_write_with_mkdir(out_path.as_std_path(), json.as_bytes())
            .map_err(McpToolError::from_io)?;
        Some(out_path)
    };

    Ok(CortexReceiptOutput {
        schema: cordance_core::schema::CORDANCE_CORTEX_RECEIPT_V1_CANDIDATE.to_string(),
        written_path,
        receipt,
    })
}

fn assert_authority_invariants(r: &CortexReceiptV1Candidate) -> McpToolResult<()> {
    let b = &r.authority_boundary;
    if !b.candidate_only
        || b.cortex_truth_allowed
        || b.cortex_admission_allowed
        || b.durable_promotion_allowed
        || b.memory_promotion_allowed
        || b.doctrine_promotion_allowed
        || b.trusted_history_allowed
        || b.release_acceptance_allowed
        || b.runtime_authority_allowed
    {
        return Err(McpToolError::Internal(
            "authority boundary widened — refused (Cordance never grants Cortex authority)".into(),
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use cordance_core::receipt::AuthorityBoundary;

    fn fixture_receipt() -> CortexReceiptV1Candidate {
        // The default builder path always uses `candidate_only`. We construct
        // a receipt directly from `AuthorityBoundary::candidate_only()` and
        // the simplest possible body so we don't need a pack on disk.
        //
        // Receipt structs in `cordance-core` are `#[non_exhaustive]`, so this
        // test fixture must build them through their `new` constructors.
        use chrono::Utc;
        use cordance_core::receipt::{
            ExecutionTrust, OperatorApproval, ReceiptBody, RuntimeIntegrity, SourceContext,
            TruthCeiling,
        };
        CortexReceiptV1Candidate::new(
            cordance_core::schema::CORDANCE_CORTEX_RECEIPT_V1_CANDIDATE.into(),
            1,
            "candidate_only".into(),
            "cortex context-pack admit-cordance".into(),
            AuthorityBoundary::candidate_only(),
            ReceiptBody::new(
                "test".into(),
                Utc::now(),
                "candidate_only".into(),
                "partial_structural_evidence".into(),
                "repo_only_no_runtime_write".into(),
                "not_bound_for_cortex_promotion".into(),
                "test".into(),
                SourceContext::new(
                    "test".into(),
                    TruthCeiling::CandidateEvidenceOnly,
                    "not_cleared_by_boundary_crossing".into(),
                ),
                ExecutionTrust::new(
                    "local_candidate_only".into(),
                    "deny_authority_grant".into(),
                    "not_field_level_bound_for_cortex".into(),
                    "not_field_level_bound_for_cortex".into(),
                ),
                RuntimeIntegrity::new(false, false, "not_requested".into()),
                OperatorApproval::new(
                    true,
                    "not_supplied_for_cortex_promotion".into(),
                    "not_supplied_for_cortex_promotion".into(),
                ),
                vec![],
                vec![],
                vec![],
                vec![],
            ),
            vec![],
        )
    }

    #[test]
    fn invariants_pass_for_candidate_only_boundary() {
        let r = fixture_receipt();
        assert!(assert_authority_invariants(&r).is_ok());
    }

    #[test]
    fn invariants_fail_when_boundary_widens() {
        let mut r = fixture_receipt();
        r.authority_boundary.cortex_truth_allowed = true;
        let err = assert_authority_invariants(&r).expect_err("widening must be refused");
        match err {
            McpToolError::Internal(msg) => assert!(msg.contains("authority")),
            other => panic!("expected Internal, got {other:?}"),
        }
    }
}