use cortex_core::{
compose_policy_outcomes, AuthorityClass, BoundaryContradictionState, ClaimCeiling,
ClaimProofState, ContextPackId, ContradictionId, CoreError, CoreResult, DoctrineId, EventId,
MemoryId, PolicyContribution, PolicyDecision, PolicyOutcome, PrincipleId, ProvenanceClass,
ReportableClaim, RuntimeMode, SemanticTrustClass,
};
use serde::{Deserialize, Serialize};
use crate::audit::{ExcludedAuditEntry, ExclusionReason, IncludedAuditEntry, SelectionAudit};
use crate::redaction::{PackMode, RawEventPayloadPolicy, RedactionPolicy, Sensitivity};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ContextRefId {
Memory {
memory_id: MemoryId,
},
Principle {
principle_id: PrincipleId,
},
Event {
event_id: EventId,
},
}
impl ContextRefId {
pub(crate) fn kind_name(&self) -> &'static str {
match self {
Self::Memory { .. } => "memory",
Self::Principle { .. } => "principle",
Self::Event { .. } => "event",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SelectedContextRef {
pub ref_id: ContextRefId,
pub summary: String,
pub scope: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authority: Option<String>,
pub runtime_mode: RuntimeMode,
pub authority_class: AuthorityClass,
pub proof_state: ClaimProofState,
pub claim_ceiling: ClaimCeiling,
pub provenance_class: ProvenanceClass,
pub semantic_trust: SemanticTrustClass,
pub downgrade_reasons: Vec<String>,
pub selection_reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_event_payload: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ContextRefCandidate {
pub ref_id: ContextRefId,
pub summary: String,
pub scope: Vec<String>,
pub confidence: Option<u8>,
pub authority: Option<String>,
pub runtime_mode: RuntimeMode,
pub authority_class: AuthorityClass,
pub proof_state: ClaimProofState,
pub requested_ceiling: ClaimCeiling,
pub provenance_class: ProvenanceClass,
pub semantic_trust: SemanticTrustClass,
pub selection_reason: String,
pub sensitivity: Sensitivity,
pub raw_event_payload: Option<serde_json::Value>,
}
impl ContextRefCandidate {
#[must_use]
pub fn new(ref_id: ContextRefId, summary: impl Into<String>) -> Self {
Self {
ref_id,
summary: summary.into(),
scope: Vec::new(),
confidence: None,
authority: None,
runtime_mode: RuntimeMode::LocalUnsigned,
authority_class: AuthorityClass::Derived,
proof_state: ClaimProofState::Unknown,
requested_ceiling: ClaimCeiling::LocalUnsigned,
provenance_class: ProvenanceClass::RuntimeDerived,
semantic_trust: SemanticTrustClass::CandidateOnly,
selection_reason: "selected_by_caller".to_string(),
sensitivity: Sensitivity::Internal,
raw_event_payload: None,
}
}
#[must_use]
pub fn with_raw_event_payload(mut self, raw_event_payload: serde_json::Value) -> Self {
self.raw_event_payload = Some(raw_event_payload);
self
}
#[must_use]
pub fn with_sensitivity(mut self, sensitivity: Sensitivity) -> Self {
self.sensitivity = sensitivity;
self
}
#[must_use]
pub fn with_claim_metadata(
mut self,
runtime_mode: RuntimeMode,
authority_class: AuthorityClass,
proof_state: ClaimProofState,
requested_ceiling: ClaimCeiling,
) -> Self {
self.runtime_mode = runtime_mode;
self.authority_class = authority_class;
self.proof_state = proof_state;
self.requested_ceiling = requested_ceiling;
self.provenance_class = provenance_from_authority_class(authority_class);
self.semantic_trust = semantic_trust_from_claim_metadata(authority_class, proof_state);
self
}
#[must_use]
pub const fn with_semantic_metadata(
mut self,
provenance_class: ProvenanceClass,
semantic_trust: SemanticTrustClass,
) -> Self {
self.provenance_class = provenance_class;
self.semantic_trust = semantic_trust;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackExclusion {
#[serde(skip_serializing_if = "Option::is_none")]
pub ref_id: Option<ContextRefId>,
pub ref_kind: String,
pub reason: ExclusionReason,
pub rationale: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackConflict {
pub contradiction_id: ContradictionId,
#[serde(default = "default_conflict_posture")]
pub posture: BoundaryContradictionState,
pub refs: Vec<ContextRefId>,
pub summary: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextPack {
pub context_pack_id: ContextPackId,
pub task: String,
pub max_tokens: usize,
pub pack_mode: PackMode,
pub redaction_policy: RedactionPolicy,
pub selected_refs: Vec<SelectedContextRef>,
pub active_doctrine_ids: Vec<DoctrineId>,
pub conflicts: Vec<PackConflict>,
pub exclusions: Vec<PackExclusion>,
pub selection_audit: SelectionAudit,
}
impl ContextPack {
#[must_use]
pub fn contradiction_posture(&self) -> BoundaryContradictionState {
if self
.conflicts
.iter()
.any(|conflict| conflict.posture == BoundaryContradictionState::Blocked)
{
BoundaryContradictionState::Blocked
} else if self
.conflicts
.iter()
.any(|conflict| conflict.posture == BoundaryContradictionState::Unknown)
{
BoundaryContradictionState::Unknown
} else if self
.conflicts
.iter()
.any(|conflict| conflict.posture == BoundaryContradictionState::MultiHypothesis)
{
BoundaryContradictionState::MultiHypothesis
} else {
BoundaryContradictionState::Resolved
}
}
#[must_use]
pub fn policy_decision(&self) -> PolicyDecision {
let mut contributions = vec![PolicyContribution::new(
"context_pack.builder.valid",
PolicyOutcome::Allow,
"context pack built under declared pack/redaction policy",
)
.expect("static policy contribution is valid")];
if self.pack_mode == PackMode::External
&& self.redaction_policy.raw_event_payloads != RawEventPayloadPolicy::Excluded
{
contributions.push(
PolicyContribution::new(
"context_pack.redaction.external_raw_payload",
PolicyOutcome::Reject,
"external context packs must exclude raw event payloads",
)
.expect("static policy contribution is valid"),
);
}
if self.pack_mode == PackMode::External
&& self
.selected_refs
.iter()
.any(|selected| selected.raw_event_payload.is_some())
{
contributions.push(
PolicyContribution::new(
"context_pack.redaction.raw_payload_leak",
PolicyOutcome::Reject,
"external context pack selected refs must not carry raw payloads",
)
.expect("static policy contribution is valid"),
);
}
if !self.conflicts.is_empty() {
contributions.push(
PolicyContribution::new(
"context_pack.conflict_present",
PolicyOutcome::Quarantine,
"context pack contains surfaced conflicts and must not be treated as clean authority",
)
.expect("static policy contribution is valid"),
);
}
compose_policy_outcomes(contributions, None)
}
pub fn require_default_use_allowed(&self) -> CoreResult<()> {
let policy = self.policy_decision();
match policy.final_outcome {
PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
Err(CoreError::Validation(format!(
"context pack default use blocked by policy outcome {:?}",
policy.final_outcome
)))
}
PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
}
}
}
const fn default_conflict_posture() -> BoundaryContradictionState {
BoundaryContradictionState::Unknown
}
#[derive(Debug, Clone)]
pub struct ContextPackBuilder {
task: String,
max_tokens: usize,
pack_mode: PackMode,
include_raw_event_payloads: bool,
selected_candidates: Vec<ContextRefCandidate>,
active_doctrine_ids: Vec<DoctrineId>,
conflicts: Vec<PackConflict>,
exclusions: Vec<ExclusionCandidate>,
}
#[derive(Debug, Clone, PartialEq)]
struct ExclusionCandidate {
ref_id: ContextRefId,
reason: ExclusionReason,
rationale: String,
sensitivity: Sensitivity,
}
impl ContextPackBuilder {
#[must_use]
pub fn new(task: impl Into<String>, max_tokens: usize) -> Self {
Self {
task: task.into(),
max_tokens,
pack_mode: PackMode::External,
include_raw_event_payloads: false,
selected_candidates: Vec::new(),
active_doctrine_ids: Vec::new(),
conflicts: Vec::new(),
exclusions: Vec::new(),
}
}
#[must_use]
pub fn pack_mode(mut self, pack_mode: PackMode) -> Self {
self.pack_mode = pack_mode;
self
}
#[must_use]
pub fn include_raw_event_payloads_in_operator_mode(mut self) -> Self {
self.include_raw_event_payloads = true;
self
}
#[must_use]
pub fn select_ref(mut self, candidate: ContextRefCandidate) -> Self {
self.selected_candidates.push(candidate);
self
}
#[must_use]
pub fn active_doctrine(mut self, doctrine_id: DoctrineId) -> Self {
self.active_doctrine_ids.push(doctrine_id);
self
}
#[must_use]
pub fn conflict(mut self, conflict: PackConflict) -> Self {
self.conflicts.push(conflict);
self
}
#[must_use]
pub fn exclude_ref(
mut self,
ref_id: ContextRefId,
reason: ExclusionReason,
rationale: impl Into<String>,
sensitivity: Sensitivity,
) -> Self {
self.exclusions.push(ExclusionCandidate {
ref_id,
reason,
rationale: rationale.into(),
sensitivity,
});
self
}
pub fn build(self) -> CoreResult<ContextPack> {
if self.task.trim().is_empty() {
return Err(CoreError::Validation(
"context pack task must not be empty".to_string(),
));
}
if self.max_tokens == 0 {
return Err(CoreError::Validation(
"context pack max_tokens must be greater than zero".to_string(),
));
}
let redaction_policy = self.redaction_policy()?;
let selected_refs = self.selected_refs(&redaction_policy);
let exclusions = self.pack_exclusions();
let estimated_tokens = estimate_pack_tokens(&self.task, &selected_refs, &exclusions);
if estimated_tokens > self.max_tokens {
return Err(CoreError::Validation(format!(
"context pack estimated token count {estimated_tokens} exceeds budget {}",
self.max_tokens
)));
}
let mut selection_audit =
SelectionAudit::new(self.pack_mode, redaction_policy.clone(), estimated_tokens);
selection_audit.included = self.included_audit();
selection_audit.exclusions = self.excluded_audit();
Ok(ContextPack {
context_pack_id: ContextPackId::new(),
task: self.task,
max_tokens: self.max_tokens,
pack_mode: self.pack_mode,
redaction_policy,
selected_refs,
active_doctrine_ids: self.active_doctrine_ids,
conflicts: self.conflicts,
exclusions,
selection_audit,
})
}
fn redaction_policy(&self) -> CoreResult<RedactionPolicy> {
match (self.pack_mode, self.include_raw_event_payloads) {
(PackMode::External, false) | (PackMode::Operator, false) => {
Ok(RedactionPolicy::external_default())
}
(PackMode::Operator, true) => Ok(RedactionPolicy::operator_with_raw_payload_opt_in()),
(PackMode::External, true) => Err(CoreError::Validation(
"raw event payload opt-in requires operator pack mode".to_string(),
)),
}
}
fn selected_refs(&self, redaction_policy: &RedactionPolicy) -> Vec<SelectedContextRef> {
self.selected_candidates
.iter()
.map(|candidate| {
let include_raw_payload = redaction_policy.raw_event_payloads
== RawEventPayloadPolicy::OperatorOptIn
&& candidate.sensitivity != Sensitivity::Secret;
let claim = ReportableClaim::new(
"context pack selected ref",
candidate.runtime_mode,
candidate.authority_class,
candidate.proof_state,
candidate.requested_ceiling,
);
let claim_ceiling = claim
.effective_ceiling()
.min(candidate.provenance_class.claim_ceiling())
.min(candidate.semantic_trust.claim_ceiling());
let mut downgrade_reasons = claim.downgrade_reasons().to_vec();
if claim_ceiling < claim.effective_ceiling() {
downgrade_reasons.push(format!(
"semantic trust {:?} and provenance {:?} limit authority claims",
candidate.semantic_trust, candidate.provenance_class
));
}
SelectedContextRef {
ref_id: candidate.ref_id.clone(),
summary: candidate.summary.clone(),
scope: candidate.scope.clone(),
confidence: candidate.confidence,
authority: candidate.authority.clone(),
runtime_mode: claim.runtime_mode(),
authority_class: claim.authority_class(),
proof_state: claim.proof_state(),
claim_ceiling,
provenance_class: candidate.provenance_class,
semantic_trust: candidate.semantic_trust,
downgrade_reasons,
selection_reason: candidate.selection_reason.clone(),
raw_event_payload: include_raw_payload
.then(|| candidate.raw_event_payload.clone())
.flatten(),
}
})
.collect()
}
fn pack_exclusions(&self) -> Vec<PackExclusion> {
self.exclusions
.iter()
.map(|exclusion| {
let ref_id = match (self.pack_mode, exclusion.sensitivity) {
(PackMode::External, Sensitivity::Personal | Sensitivity::Secret) => None,
_ => Some(exclusion.ref_id.clone()),
};
PackExclusion {
ref_kind: exclusion.ref_id.kind_name().to_string(),
ref_id,
reason: exclusion.reason,
rationale: exclusion.rationale.clone(),
}
})
.collect()
}
fn included_audit(&self) -> Vec<IncludedAuditEntry> {
self.selected_candidates
.iter()
.map(|candidate| IncludedAuditEntry {
ref_id: candidate.ref_id.clone(),
rule_id: "context_pack.builder.selected_by_caller.v1".to_string(),
reason: candidate.selection_reason.clone(),
sensitivity: candidate.sensitivity,
})
.collect()
}
fn excluded_audit(&self) -> Vec<ExcludedAuditEntry> {
self.exclusions
.iter()
.map(|exclusion| {
let ref_id = match (self.pack_mode, exclusion.sensitivity) {
(PackMode::External, Sensitivity::Personal | Sensitivity::Secret) => None,
_ => Some(exclusion.ref_id.clone()),
};
ExcludedAuditEntry {
ref_kind: exclusion.ref_id.kind_name().to_string(),
ref_id,
reason: exclusion.reason,
rule_id: "context_pack.builder.excluded_by_caller.v1".to_string(),
rationale: exclusion.rationale.clone(),
sensitivity: exclusion.sensitivity,
}
})
.collect()
}
}
const fn provenance_from_authority_class(authority_class: AuthorityClass) -> ProvenanceClass {
match authority_class {
AuthorityClass::Untrusted => ProvenanceClass::UnknownProvenance,
AuthorityClass::Derived => ProvenanceClass::RuntimeDerived,
AuthorityClass::Observed | AuthorityClass::Verified => ProvenanceClass::ToolObserved,
AuthorityClass::Operator => ProvenanceClass::OperatorAttested,
}
}
const fn semantic_trust_from_claim_metadata(
authority_class: AuthorityClass,
proof_state: ClaimProofState,
) -> SemanticTrustClass {
match (authority_class, proof_state) {
(AuthorityClass::Operator, ClaimProofState::FullChainVerified)
| (
AuthorityClass::Verified | AuthorityClass::Observed,
ClaimProofState::FullChainVerified,
) => SemanticTrustClass::SingleFamily,
(AuthorityClass::Derived, _) => SemanticTrustClass::CandidateOnly,
_ => SemanticTrustClass::Unknown,
}
}
fn estimate_pack_tokens(
task: &str,
selected_refs: &[SelectedContextRef],
exclusions: &[PackExclusion],
) -> usize {
let chars = task.len()
+ selected_refs
.iter()
.map(|r| r.summary.len() + r.scope.iter().map(String::len).sum::<usize>())
.sum::<usize>()
+ exclusions
.iter()
.map(|e| e.rationale.len() + e.ref_kind.len())
.sum::<usize>();
chars.div_ceil(4).max(1)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn event_ref() -> ContextRefId {
ContextRefId::Event {
event_id: EventId::new(),
}
}
fn memory_ref() -> ContextRefId {
ContextRefId::Memory {
memory_id: MemoryId::new(),
}
}
#[test]
fn default_external_pack_excludes_raw_event_payload_fields() {
let pack = ContextPackBuilder::new("prepare safe context", 512)
.select_ref(
ContextRefCandidate::new(event_ref(), "operator prefers concise summaries")
.with_raw_event_payload(json!({
"payload_json": {
"private_text": "do not export"
}
})),
)
.build()
.expect("build external pack");
let serialized = serde_json::to_value(&pack).expect("serialize pack");
assert_eq!(serialized["pack_mode"], json!("external"));
assert_eq!(
serialized["redaction_policy"]["raw_event_payloads"],
json!("excluded")
);
assert!(!contains_object_key(&serialized, "raw_event_payload"));
assert!(!contains_object_key(&serialized, "payload_json"));
}
#[test]
fn default_external_pack_records_mode_policy_and_exclusion() {
let pack = ContextPackBuilder::new("prepare safe context", 512)
.select_ref(ContextRefCandidate::new(
memory_ref(),
"prefer explicit evidence",
))
.exclude_ref(
event_ref(),
ExclusionReason::RedactionPolicy,
"raw private event omitted from external pack",
Sensitivity::Personal,
)
.build()
.expect("build external pack");
assert_eq!(pack.pack_mode, PackMode::External);
assert_eq!(pack.redaction_policy, RedactionPolicy::external_default());
assert_eq!(pack.exclusions.len(), 1);
assert_eq!(pack.exclusions[0].reason, ExclusionReason::RedactionPolicy);
assert_eq!(pack.exclusions[0].ref_id, None);
assert_eq!(pack.selection_audit.exclusions.len(), 1);
assert_eq!(pack.selection_audit.exclusions[0].ref_id, None);
assert_eq!(pack.selection_audit.pack_mode, PackMode::External);
assert_eq!(
pack.selection_audit.redaction_policy,
RedactionPolicy::external_default()
);
assert_eq!(pack.policy_decision().final_outcome, PolicyOutcome::Allow);
}
#[test]
fn selected_refs_expose_truth_ceiling_metadata() {
let pack = ContextPackBuilder::new("prepare safe context", 512)
.select_ref(ContextRefCandidate::new(
memory_ref(),
"prefer explicit evidence",
))
.build()
.expect("build external pack");
let selected = &pack.selected_refs[0];
assert_eq!(selected.runtime_mode, RuntimeMode::LocalUnsigned);
assert_eq!(selected.authority_class, AuthorityClass::Derived);
assert_eq!(selected.proof_state, ClaimProofState::Unknown);
assert_eq!(selected.claim_ceiling, ClaimCeiling::DevOnly);
assert_eq!(selected.provenance_class, ProvenanceClass::RuntimeDerived);
assert_eq!(selected.semantic_trust, SemanticTrustClass::CandidateOnly);
assert!(selected
.downgrade_reasons
.iter()
.any(|reason| reason.contains("proof state Unknown")));
let serialized = serde_json::to_value(&pack).expect("serialize pack");
assert_eq!(
serialized["selected_refs"][0]["proof_state"],
json!("unknown")
);
assert_eq!(
serialized["selected_refs"][0]["claim_ceiling"],
json!("dev_only")
);
assert_eq!(
serialized["selected_refs"][0]["provenance_class"],
json!("runtime_derived")
);
assert_eq!(
serialized["selected_refs"][0]["semantic_trust"],
json!("candidate_only")
);
}
#[test]
fn selected_refs_expose_explicit_semantic_trust_metadata() {
let pack = ContextPackBuilder::new("prepare safe context", 512)
.select_ref(
ContextRefCandidate::new(memory_ref(), "operator falsified support")
.with_claim_metadata(
RuntimeMode::AuthorityGrade,
AuthorityClass::Operator,
ClaimProofState::FullChainVerified,
ClaimCeiling::AuthorityGrade,
)
.with_semantic_metadata(
ProvenanceClass::OperatorAttested,
SemanticTrustClass::FalsificationTested,
),
)
.build()
.expect("build external pack");
let selected = &pack.selected_refs[0];
assert_eq!(selected.provenance_class, ProvenanceClass::OperatorAttested);
assert_eq!(
selected.semantic_trust,
SemanticTrustClass::FalsificationTested
);
assert_eq!(selected.claim_ceiling, ClaimCeiling::AuthorityGrade);
}
#[test]
fn conflict_pack_policy_decision_is_quarantine() {
let ref_id = memory_ref();
let pack = ContextPackBuilder::new("prepare conflicted context", 512)
.select_ref(ContextRefCandidate::new(
ref_id.clone(),
"conflicted memory",
))
.conflict(PackConflict {
contradiction_id: ContradictionId::new(),
posture: BoundaryContradictionState::Blocked,
refs: vec![ref_id],
summary: "memory conflicts with a newer claim".into(),
})
.build()
.expect("build pack");
let policy = pack.policy_decision();
assert_eq!(policy.final_outcome, PolicyOutcome::Quarantine);
assert_eq!(
policy.contributing[0].rule_id.as_str(),
"context_pack.conflict_present"
);
}
#[test]
fn conflict_pack_fails_closed_for_default_use() {
let ref_id = memory_ref();
let pack = ContextPackBuilder::new("prepare conflicted context", 512)
.select_ref(ContextRefCandidate::new(
ref_id.clone(),
"conflicted memory",
))
.conflict(PackConflict {
contradiction_id: ContradictionId::new(),
posture: BoundaryContradictionState::Blocked,
refs: vec![ref_id],
summary: "memory conflicts with a newer claim".into(),
})
.build()
.expect("build diagnostic pack");
let err = pack
.require_default_use_allowed()
.expect_err("conflicted pack must not be default-usable");
assert!(
err.to_string().contains("Quarantine"),
"default-use failure should expose the policy outcome: {err}"
);
}
#[test]
fn clean_external_pack_is_default_usable() {
let pack = ContextPackBuilder::new("prepare safe context", 512)
.select_ref(ContextRefCandidate::new(
memory_ref(),
"prefer explicit evidence",
))
.build()
.expect("build external pack");
pack.require_default_use_allowed()
.expect("clean pack is default-usable");
}
#[test]
fn conflict_posture_carries_unknown_and_multi_hypothesis_advisories() {
let unknown_ref = memory_ref();
let multi_hypothesis_ref = event_ref();
let pack = ContextPackBuilder::new("prepare conflicted context", 768)
.select_ref(ContextRefCandidate::new(
unknown_ref.clone(),
"conflict resolver could not prove precedence",
))
.conflict(PackConflict {
contradiction_id: ContradictionId::new(),
posture: BoundaryContradictionState::Unknown,
refs: vec![unknown_ref],
summary: "precedence proof is missing".into(),
})
.conflict(PackConflict {
contradiction_id: ContradictionId::new(),
posture: BoundaryContradictionState::MultiHypothesis,
refs: vec![multi_hypothesis_ref],
summary: "both hypotheses remain live".into(),
})
.build()
.expect("build diagnostic pack");
assert_eq!(
pack.contradiction_posture(),
BoundaryContradictionState::Unknown
);
assert_eq!(
pack.policy_decision().final_outcome,
PolicyOutcome::Quarantine
);
let serialized = serde_json::to_value(&pack).expect("serialize pack");
assert_eq!(serialized["conflicts"][0]["posture"], json!("unknown"));
assert_eq!(
serialized["conflicts"][1]["posture"],
json!("multi_hypothesis")
);
}
fn contains_object_key(value: &serde_json::Value, needle: &str) -> bool {
match value {
serde_json::Value::Object(map) => map
.iter()
.any(|(key, value)| key == needle || contains_object_key(value, needle)),
serde_json::Value::Array(values) => values
.iter()
.any(|value| contains_object_key(value, needle)),
_ => false,
}
}
}