use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use cortex_core::attestor::{attest, verify, Attestor, InMemoryAttestor};
use cortex_core::canonical::{
AttestationPreimage, LineageBinding, SourceIdentity, SCHEMA_VERSION_ATTESTATION,
};
use cortex_core::{
compose_policy_outcomes, evaluate_semantic_trust, AuditRecordId, AuthorityClass, ClaimCeiling,
ClaimProofState, DoctrineId, MemoryId, PolicyContribution, PolicyDecision, PolicyOutcome,
PrincipleId, ProofState, ProvenanceClass, RuntimeMode, SemanticTrustInput, SemanticUse,
TrustTier,
};
use cortex_ledger::payload_hash;
use cortex_retrieval::{
resolve_conflicts, AuthorityLevel, AuthorityProofHint, ConflictingMemoryInput, ProofClosureHint,
};
use cortex_runtime::{require_runtime_claim_with_policy, RuntimeClaimKind};
use cortex_store::proof::verify_memory_proof_closure;
use cortex_store::repo::{
AuthorityRepo, ContradictionRepo, DoctrinePromotion, MemoryRecord, MemoryRepo, PrincipleRepo,
TemporalAuthorityQuery,
};
use serde_json::json;
use crate::cmd::open_default_store;
use crate::exit::Exit;
#[derive(Debug, Subcommand)]
pub enum PrincipleSub {
Promote(PromoteArgs),
VerifyPromotion(VerifyPromotionArgs),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum PromoteForce {
Advisory,
Conditioning,
Gate,
}
#[derive(Debug, Args)]
pub struct PromoteArgs {
#[arg(value_name = "PRINCIPLE_ID")]
pub principle_id: PrincipleId,
#[arg(long, value_enum)]
pub force: PromoteForce,
#[arg(long)]
pub reason: String,
#[arg(long, value_name = "KEY_PATH")]
pub attestation: Option<PathBuf>,
}
#[derive(Debug, Args)]
pub struct VerifyPromotionArgs {
#[arg(value_name = "PRINCIPLE_ID")]
pub principle_id: PrincipleId,
}
pub fn run(sub: PrincipleSub) -> Exit {
match sub {
PrincipleSub::Promote(args) => promote(args),
PrincipleSub::VerifyPromotion(args) => verify_promotion(args),
}
}
fn promote(args: PromoteArgs) -> Exit {
if args.reason.trim().is_empty() {
eprintln!("cortex principle promote: --reason must not be empty");
return Exit::Usage;
}
let pool = match open_default_store("principle promote") {
Ok(pool) => pool,
Err(exit) => return exit,
};
let Some(attestation_path) = args.attestation.as_ref() else {
eprintln!(
"cortex principle promote: --attestation <KEY_PATH> is required for promoted durable authority; no state was changed"
);
return Exit::PreconditionUnmet;
};
let attestor = match load_attestor_from_key_file(attestation_path) {
Ok(attestor) => attestor,
Err(exit) => return exit,
};
let policy = match promotion_policy_decision(&pool, &args.principle_id) {
Ok(policy) => policy,
Err(exit) => return exit,
};
if let Err(preflight) = require_runtime_claim_with_policy(
"attested doctrine promotion",
RuntimeClaimKind::Promotion,
RuntimeMode::AuthorityGrade,
AuthorityClass::Operator,
ClaimProofState::FullChainVerified,
ClaimCeiling::AuthorityGrade,
&policy,
) {
eprintln!(
"cortex principle promote: promotion preflight failed closed; no state was changed"
);
match serde_json::to_string(&json!({
"error": "promotion_preflight_failed",
"reason": preflight.reason,
"runtime_mode": preflight.claim.runtime_mode,
"claim_ceiling": preflight.claim.effective_ceiling,
"required_ceiling": preflight.claim.required_ceiling,
"policy_outcome": policy.final_outcome,
"policy_contributing": policy.contributing,
})) {
Ok(serialized) => eprintln!("{serialized}"),
Err(err) => eprintln!(
"cortex principle promote: failed to serialize promotion preflight failure: {err}"
),
}
return Exit::PreconditionUnmet;
}
if let Err(exit) = require_operator_temporal_authority(&pool, &attestor) {
return exit;
}
let actor_json = match promotion_actor_json(&pool, &args, &attestor) {
Ok(actor_json) => actor_json,
Err(exit) => return exit,
};
let repo = PrincipleRepo::new(&pool);
let promotion = DoctrinePromotion {
doctrine_id: DoctrineId::new(),
audit_id: AuditRecordId::new(),
source_principle: args.principle_id,
force: args.force.as_store_value().to_string(),
reason: args.reason,
promoted_by_json: actor_json,
created_at: chrono::Utc::now(),
};
match repo.promote_to_doctrine(&promotion, &policy) {
Ok(doctrine) => {
println!(
"cortex principle promote: promoted {} to doctrine {}",
doctrine.source_principle, doctrine.id
);
Exit::Ok
}
Err(err) => {
eprintln!("cortex principle promote: {err}; no state was changed");
Exit::PreconditionUnmet
}
}
}
fn require_operator_temporal_authority(
pool: &cortex_store::Pool,
attestor: &InMemoryAttestor,
) -> Result<(), Exit> {
let now = chrono::Utc::now();
let report = AuthorityRepo::new(pool)
.revalidate(&TemporalAuthorityQuery {
key_id: attestor.key_id().to_string(),
event_time: now,
now,
minimum_trust_tier: TrustTier::Verified,
})
.map_err(|err| {
eprintln!(
"cortex principle promote: failed to revalidate operator temporal authority: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
if let Err(err) = report.require_current_use_allowed() {
eprintln!(
"cortex principle promote: temporal authority preflight failed closed: {err}; no state was changed"
);
match serde_json::to_string(&json!({
"error": "temporal_authority_preflight_failed",
"key_id": report.key_id,
"principal_id": report.principal_id,
"valid_at_event_time": report.valid_at_event_time,
"valid_now": report.valid_now,
"invalidated_after": report.invalidated_after,
"reasons": report.reasons,
"policy_outcome": report.policy_decision().final_outcome,
})) {
Ok(serialized) => eprintln!("{serialized}"),
Err(err) => eprintln!(
"cortex principle promote: failed to serialize temporal authority failure: {err}"
),
}
return Err(Exit::PreconditionUnmet);
}
Ok(())
}
fn promotion_policy_decision(
pool: &cortex_store::Pool,
principle_id: &PrincipleId,
) -> Result<PolicyDecision, Exit> {
let repo = PrincipleRepo::new(pool);
let principle = repo.get_by_id(principle_id).map_err(|err| {
eprintln!(
"cortex principle promote: failed to read promotion policy inputs: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
let Some(principle) = principle else {
let contribution = policy_contribution(
"principle_promotion.exists",
PolicyOutcome::Reject,
format!("principle {principle_id} not found"),
)?;
return Ok(compose_policy_outcomes(vec![contribution], None));
};
let (outcome, reason) = promotion_falsification_policy(&principle.created_by_json);
let mut contributions = vec![policy_contribution(
"principle_promotion.falsification",
outcome,
reason,
)?];
contributions.extend(supporting_memory_proof_contributions(
pool,
&principle.supporting_memories_json,
)?);
contributions.extend(supporting_memory_conflict_contributions(
pool,
&principle.supporting_memories_json,
)?);
contributions.extend(supporting_memory_semantic_contributions(
pool,
&principle.supporting_memories_json,
&principle.created_by_json,
)?);
Ok(compose_policy_outcomes(contributions, None))
}
fn supporting_memory_proof_contributions(
pool: &cortex_store::Pool,
supporting_memories: &serde_json::Value,
) -> Result<Vec<PolicyContribution>, Exit> {
let Some(memory_ids) = supporting_memories.as_array() else {
return Ok(vec![policy_contribution(
"principle_promotion.supporting_memory_proof",
PolicyOutcome::Reject,
"principle supporting_memories_json must be an array",
)?]);
};
let mut contributions = Vec::with_capacity(memory_ids.len().max(1));
if memory_ids.is_empty() {
contributions.push(policy_contribution(
"principle_promotion.supporting_memory_proof",
PolicyOutcome::Reject,
"doctrine promotion requires supporting memories",
)?);
}
for value in memory_ids {
let Some(raw_id) = value.as_str() else {
contributions.push(policy_contribution(
"principle_promotion.supporting_memory_proof",
PolicyOutcome::Reject,
"supporting memory reference is not a string",
)?);
continue;
};
let Ok(memory_id) = raw_id.parse::<MemoryId>() else {
contributions.push(policy_contribution(
"principle_promotion.supporting_memory_proof",
PolicyOutcome::Reject,
format!("supporting memory reference {raw_id} is not a valid memory id"),
)?);
continue;
};
let report = verify_memory_proof_closure(pool, &memory_id).map_err(|err| {
eprintln!(
"cortex principle promote: failed to verify supporting memory proof closure for {memory_id}: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
let policy = report.policy_decision();
contributions.push(policy_contribution(
"principle_promotion.supporting_memory_proof",
policy.final_outcome,
format!(
"supporting memory {memory_id} proof closure is {:?}",
report.state()
),
)?);
}
Ok(contributions)
}
fn supporting_memory_conflict_contributions(
pool: &cortex_store::Pool,
supporting_memories: &serde_json::Value,
) -> Result<Vec<PolicyContribution>, Exit> {
let Some(memory_ids) = supporting_memories.as_array() else {
return Ok(Vec::new());
};
let support_ids = memory_ids
.iter()
.filter_map(serde_json::Value::as_str)
.map(str::to_owned)
.collect::<BTreeSet<_>>();
if support_ids.is_empty() {
return Ok(Vec::new());
}
let contradictions = ContradictionRepo::new(pool).list_open().map_err(|err| {
eprintln!(
"cortex principle promote: failed to read supporting memory contradictions: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
let mut affected_ids = BTreeSet::new();
let mut conflict_edges = BTreeMap::<String, BTreeSet<String>>::new();
for contradiction in contradictions {
let left_support = support_ids.contains(&contradiction.left_ref);
let right_support = support_ids.contains(&contradiction.right_ref);
if !left_support && !right_support {
continue;
}
affected_ids.insert(contradiction.left_ref.clone());
affected_ids.insert(contradiction.right_ref.clone());
conflict_edges
.entry(contradiction.left_ref.clone())
.or_default()
.insert(contradiction.right_ref.clone());
conflict_edges
.entry(contradiction.right_ref)
.or_default()
.insert(contradiction.left_ref);
}
if affected_ids.is_empty() {
return Ok(vec![policy_contribution(
"principle_promotion.supporting_memory_conflict_resolution",
PolicyOutcome::Allow,
"supporting memories have no open durable contradictions",
)?]);
}
let memory_repo = MemoryRepo::new(pool);
let mut memory_records = BTreeMap::<String, MemoryRecord>::new();
for id in &affected_ids {
let Ok(memory_id) = id.parse::<MemoryId>() else {
return Ok(vec![policy_contribution(
"principle_promotion.supporting_memory_conflict_resolution",
PolicyOutcome::Reject,
format!("open contradiction references invalid memory id {id}"),
)?]);
};
let Some(memory) = memory_repo.get_by_id(&memory_id).map_err(|err| {
eprintln!(
"cortex principle promote: failed to read contradiction memory {memory_id}: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?
else {
return Ok(vec![policy_contribution(
"principle_promotion.supporting_memory_conflict_resolution",
PolicyOutcome::Reject,
format!("open contradiction references missing memory {id}"),
)?]);
};
memory_records.insert(id.clone(), memory);
}
let mut inputs = Vec::with_capacity(affected_ids.len());
for id in &affected_ids {
let Some(memory) = memory_records.get(id) else {
continue;
};
inputs.push(
ConflictingMemoryInput::new(
memory.id.to_string(),
Some(memory.id.to_string()),
memory.claim.clone(),
AuthorityProofHint {
authority: authority_level(&memory.authority),
proof: proof_closure_hint_for_memory(pool, &memory.id)?,
},
)
.with_conflicts(
conflict_edges
.get(&memory.id.to_string())
.map(|ids| ids.iter().cloned().collect())
.unwrap_or_default(),
),
);
}
let output = resolve_conflicts(&inputs, &[]);
let policy = output.policy_decision();
let resolver_rules = policy
.contributing
.iter()
.map(|contribution| contribution.rule_id.as_str())
.collect::<Vec<_>>()
.join(",");
Ok(vec![policy_contribution(
"principle_promotion.supporting_memory_conflict_resolution",
policy.final_outcome,
format!(
"supporting memory conflict resolver returned {:?} for {} affected memories; resolver_rules={resolver_rules}",
output.state,
affected_ids.len()
),
)?])
}
fn proof_closure_hint_for_memory(
pool: &cortex_store::Pool,
memory_id: &MemoryId,
) -> Result<ProofClosureHint, Exit> {
let report = verify_memory_proof_closure(pool, memory_id).map_err(|err| {
eprintln!(
"cortex principle promote: failed to verify contradiction memory proof closure for {memory_id}: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
Ok(match report.state() {
ProofState::FullChainVerified => ProofClosureHint::FullChainVerified,
ProofState::Partial => ProofClosureHint::Partial,
ProofState::Broken => {
let edge = report
.failing_edges()
.first()
.map(|edge| format!("{:?}:{}:{}", edge.kind, edge.from_ref, edge.reason))
.unwrap_or_else(|| "proof closure failed without a named edge".to_string());
ProofClosureHint::Broken { edge }
}
})
}
fn authority_level(authority: &str) -> AuthorityLevel {
match authority {
"user" | "operator" => AuthorityLevel::High,
"tool" | "system" => AuthorityLevel::Medium,
_ => AuthorityLevel::Low,
}
}
fn supporting_memory_semantic_contributions(
pool: &cortex_store::Pool,
supporting_memories: &serde_json::Value,
created_by: &serde_json::Value,
) -> Result<Vec<PolicyContribution>, Exit> {
let Some(memory_ids) = supporting_memories.as_array() else {
return Ok(Vec::new());
};
if memory_ids.is_empty() {
return Ok(Vec::new());
}
let memory_repo = MemoryRepo::new(pool);
let mut provenance_classes = Vec::with_capacity(memory_ids.len());
let mut independent_sources = BTreeSet::<String>::new();
for value in memory_ids {
let Some(raw_id) = value.as_str() else {
continue;
};
let Ok(memory_id) = raw_id.parse::<MemoryId>() else {
continue;
};
let Some(memory) = memory_repo.get_by_id(&memory_id).map_err(|err| {
eprintln!(
"cortex principle promote: failed to read supporting memory semantic provenance for {memory_id}: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?
else {
continue;
};
independent_sources.insert(memory.id.to_string());
provenance_classes.push(provenance_from_authority(&memory.authority));
}
let falsification_evidence =
promotion_falsification_policy(created_by).0 == PolicyOutcome::Allow;
let unresolved_unknowns = provenance_classes.contains(&ProvenanceClass::UnknownProvenance);
let report = evaluate_semantic_trust(
&SemanticTrustInput::new(SemanticUse::HighForceDoctrine)
.with_provenance(provenance_classes)
.with_independent_source_families(independent_sources.len() as u16)
.with_falsification_evidence(falsification_evidence)
.with_unresolved_unknowns(unresolved_unknowns),
);
Ok(vec![policy_contribution(
"principle_promotion.semantic_support",
report.policy_outcome,
format!(
"{}; semantic_trust={}; weakest_provenance={}; claim_ceiling={}",
report
.reasons
.first()
.map(String::as_str)
.unwrap_or("semantic support evaluated"),
wire_string(report.semantic_trust),
wire_string(report.weakest_provenance),
wire_string(report.claim_ceiling),
),
)?])
}
fn provenance_from_authority(authority: &str) -> ProvenanceClass {
match authority {
"user" | "operator" => ProvenanceClass::OperatorAttested,
"tool" | "system" => ProvenanceClass::ToolObserved,
"runtime" | "axiom" => ProvenanceClass::RuntimeDerived,
"summary" | "derived_summary" => ProvenanceClass::SummaryDerived,
"external" | "claimed" => ProvenanceClass::ExternalClaimed,
"simulated" | "hypothetical" => ProvenanceClass::SimulatedOrHypothetical,
_ => ProvenanceClass::UnknownProvenance,
}
}
fn wire_string<T: serde::Serialize>(value: T) -> String {
serde_json::to_value(value)
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.unwrap_or_else(|| "unknown".to_string())
}
fn policy_contribution(
rule_id: &'static str,
outcome: PolicyOutcome,
reason: impl Into<String>,
) -> Result<PolicyContribution, Exit> {
PolicyContribution::new(rule_id, outcome, reason).map_err(|err| {
eprintln!(
"cortex principle promote: failed to build promotion policy decision: {err:?}; no state was changed"
);
Exit::Internal
})
}
fn promotion_falsification_policy(created_by: &serde_json::Value) -> (PolicyOutcome, String) {
let Some(falsification) = created_by.get("falsification") else {
return (
PolicyOutcome::Reject,
"doctrine promotion requires recorded principle falsification attempt".into(),
);
};
if falsification
.get("status")
.and_then(serde_json::Value::as_str)
!= Some("attempted")
{
return (
PolicyOutcome::Reject,
"doctrine promotion falsification status must be attempted".into(),
);
}
let Some(failed_attempts) = falsification
.get("failed_attempts")
.and_then(serde_json::Value::as_array)
else {
return (
PolicyOutcome::Reject,
"doctrine promotion requires falsification failed_attempts".into(),
);
};
if failed_attempts.is_empty()
|| failed_attempts
.iter()
.any(|attempt| attempt.as_str().is_none_or(|text| text.trim().is_empty()))
{
return (
PolicyOutcome::Reject,
"doctrine promotion requires at least one non-empty failed falsification attempt"
.into(),
);
}
let Some(unresolved_high_risk) = falsification
.get("unresolved_high_risk_counterexamples")
.and_then(serde_json::Value::as_array)
else {
return (
PolicyOutcome::Reject,
"doctrine promotion requires unresolved_high_risk_counterexamples".into(),
);
};
if !unresolved_high_risk.is_empty() {
return (
PolicyOutcome::Reject,
"doctrine promotion blocked by unresolved high-risk counterexample".into(),
);
}
(
PolicyOutcome::Allow,
"principle promotion falsification record is complete".into(),
)
}
fn verify_promotion(args: VerifyPromotionArgs) -> Exit {
let pool = match open_default_store("principle verify-promotion") {
Ok(pool) => pool,
Err(exit) => return exit,
};
let repo = PrincipleRepo::new(&pool);
match repo.verify_promotion_attestation_replay(&args.principle_id) {
Ok(replay) => {
let payload = json!({
"principle_id": replay.source_principle,
"doctrine_id": replay.doctrine_id,
"audit_id": replay.audit_id,
"promotion_attestation_replay_verified": replay.verified,
});
match serde_json::to_string_pretty(&payload) {
Ok(serialized) => {
println!("{serialized}");
Exit::Ok
}
Err(err) => {
eprintln!(
"cortex principle verify-promotion: failed to serialize replay result: {err}"
);
Exit::Internal
}
}
}
Err(err) => {
eprintln!("cortex principle verify-promotion: {err}");
Exit::PreconditionUnmet
}
}
}
fn load_attestor_from_key_file(path: &Path) -> Result<InMemoryAttestor, Exit> {
let bytes = fs::read(path).map_err(|err| {
eprintln!(
"cortex principle promote: cannot read --attestation key file `{}`: {err}; no state was changed",
path.display()
);
Exit::PreconditionUnmet
})?;
if bytes.len() != 32 {
eprintln!(
"cortex principle promote: --attestation key file `{}` must be exactly 32 raw bytes (Ed25519 seed); got {} bytes; no state was changed",
path.display(),
bytes.len()
);
return Err(Exit::PreconditionUnmet);
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&bytes);
Ok(InMemoryAttestor::from_seed(&seed))
}
fn promotion_actor_json(
pool: &cortex_store::Pool,
args: &PromoteArgs,
attestor: &InMemoryAttestor,
) -> Result<serde_json::Value, Exit> {
let chain_position = pool
.query_row(
"SELECT COUNT(*) FROM audit_records WHERE operation = 'doctrine_promotion';",
[],
|row| row.get::<_, u64>(0),
)
.map_err(|err| {
eprintln!(
"cortex principle promote: failed to read promotion audit chain position: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
let signed_at = chrono::Utc::now();
let payload = promotion_attestation_payload(args);
let preimage = AttestationPreimage {
schema_version: SCHEMA_VERSION_ATTESTATION,
source: SourceIdentity::User,
event_id: format!("principle_promote:{}", args.principle_id),
payload_hash: payload_hash(&payload),
session_id: "principle_promote".to_string(),
ledger_id: "cortex-principle-promote".to_string(),
lineage: LineageBinding::ChainPosition(chain_position),
signed_at,
key_id: attestor.key_id().to_string(),
};
let attestation = attest(&preimage, attestor);
verify(
&preimage,
&attestation,
&attestor.verifying_key(),
attestor.key_id(),
)
.map_err(|err| {
eprintln!(
"cortex principle promote: attestation verification failed: {err}; no state was changed"
);
Exit::PreconditionUnmet
})?;
Ok(json!({
"kind": "operator",
"path": "cli",
"command": "principle promote",
"attestation_verified": true,
"attestation": {
"schema_version": preimage.schema_version,
"source": "user",
"key_id": attestation.key_id,
"public_key_hex": hex_lower(&attestor.verifying_key().to_bytes()),
"signature_hex": hex_lower(&attestation.signature),
"signed_at": attestation.signed_at,
"payload_hash": preimage.payload_hash,
"event_id": preimage.event_id,
"session_id": preimage.session_id,
"ledger_id": preimage.ledger_id,
"lineage": {
"kind": "chain_position",
"value": chain_position
}
}
}))
}
fn promotion_attestation_payload(args: &PromoteArgs) -> serde_json::Value {
json!({
"operation": "principle.promote",
"principle_id": args.principle_id,
"force": args.force.as_store_value(),
"reason": args.reason,
})
}
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
impl PromoteForce {
fn as_store_value(self) -> &'static str {
match self {
Self::Advisory => "Advisory",
Self::Conditioning => "Conditioning",
Self::Gate => "Gate",
}
}
}