use super::ir::{EvidencePolicy, MetadataDisclosure, PrivacyPolicy, ReplySpaceRule};
use super::subject::SubjectPattern;
use crate::util::DetHasher;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::hash::Hasher;
use std::time::Duration;
use thiserror::Error;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum MorphismClass {
Authoritative,
#[default]
DerivedView,
Egress,
Delegation,
}
impl MorphismClass {
pub const ALL: [Self; 4] = [
Self::Authoritative,
Self::DerivedView,
Self::Egress,
Self::Delegation,
];
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum FabricCapability {
#[default]
RewriteNamespace,
CarryAuthority,
ReplyAuthority,
ObserveEvidence,
DelegateNamespace,
CrossBoundaryEgress,
}
impl FabricCapability {
pub const ALL: [Self; 6] = [
Self::RewriteNamespace,
Self::CarryAuthority,
Self::ReplyAuthority,
Self::ObserveEvidence,
Self::DelegateNamespace,
Self::CrossBoundaryEgress,
];
#[must_use]
pub const fn is_authority_bearing(self) -> bool {
matches!(
self,
Self::CarryAuthority | Self::ReplyAuthority | Self::DelegateNamespace
)
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ReversibilityRequirement {
#[default]
EvidenceBacked,
Bijective,
Irreversible,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum SharingPolicy {
#[default]
Private,
TenantScoped,
Federated,
PublicRead,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum ResponsePolicy {
#[default]
PreserveCallerReplies,
ReplyAuthoritative,
ForwardOpaque,
StripReplies,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SubjectTransform {
#[default]
Identity,
RenamePrefix {
from: SubjectPattern,
to: SubjectPattern,
},
RedactLiterals,
SummarizeTail {
preserve_segments: usize,
},
HashPartition {
buckets: u16,
},
WildcardCapture {
index: usize,
},
DeterministicHash {
buckets: u16,
source_indices: Vec<usize>,
},
SplitSlice {
index: usize,
delimiter: String,
start: usize,
len: usize,
},
LeftExtract {
index: usize,
len: usize,
},
RightExtract {
index: usize,
len: usize,
},
Compose {
steps: Vec<Self>,
},
}
impl SubjectTransform {
#[must_use]
pub fn is_lossy(&self) -> bool {
match self {
Self::Identity | Self::RenamePrefix { .. } => false,
Self::Compose { steps } => steps.iter().any(Self::is_lossy),
Self::RedactLiterals
| Self::SummarizeTail { .. }
| Self::HashPartition { .. }
| Self::WildcardCapture { .. }
| Self::DeterministicHash { .. }
| Self::SplitSlice { .. }
| Self::LeftExtract { .. }
| Self::RightExtract { .. } => true,
}
}
#[must_use]
pub fn is_invertible(&self) -> bool {
self.inverse().is_some()
}
#[must_use]
pub fn inverse(&self) -> Option<Self> {
match self {
Self::Identity => Some(Self::Identity),
Self::RenamePrefix { from, to } => Some(Self::RenamePrefix {
from: to.clone(),
to: from.clone(),
}),
Self::Compose { steps } => {
let mut inverse_steps = Vec::with_capacity(steps.len());
for step in steps.iter().rev() {
inverse_steps.push(step.inverse()?);
}
Some(Self::Compose {
steps: inverse_steps,
})
}
Self::RedactLiterals
| Self::SummarizeTail { .. }
| Self::HashPartition { .. }
| Self::WildcardCapture { .. }
| Self::DeterministicHash { .. }
| Self::SplitSlice { .. }
| Self::LeftExtract { .. }
| Self::RightExtract { .. } => None,
}
}
pub fn apply_tokens(&self, tokens: &[String]) -> Result<Vec<String>, MorphismEvaluationError> {
match self {
Self::Identity => Ok(tokens.to_vec()),
Self::RenamePrefix { from, to } => {
let from_literals = literal_only_segments(from)?;
let to_literals = literal_only_segments(to)?;
if tokens.starts_with(&from_literals) {
let mut rewritten = to_literals;
rewritten.extend_from_slice(&tokens[from_literals.len()..]);
Ok(rewritten)
} else {
Ok(tokens.to_vec())
}
}
Self::RedactLiterals => Ok(tokens.iter().map(|_| String::from("_")).collect()),
Self::SummarizeTail { preserve_segments } => {
if tokens.len() <= *preserve_segments {
return Ok(tokens.to_vec());
}
let mut summarized = tokens[..*preserve_segments].to_vec();
summarized.push(String::from("..."));
Ok(summarized)
}
Self::HashPartition { buckets } => Ok(vec![
deterministic_bucket(tokens, &[], *buckets)?.to_string(),
]),
Self::WildcardCapture { index } => Ok(vec![select_token(tokens, *index)?.to_owned()]),
Self::DeterministicHash {
buckets,
source_indices,
} => Ok(vec![
deterministic_bucket(tokens, source_indices, *buckets)?.to_string(),
]),
Self::SplitSlice {
index,
delimiter,
start,
len,
} => {
let token = select_token(tokens, *index)?;
let pieces = token.split(delimiter).collect::<Vec<_>>();
if *start >= pieces.len() {
return Ok(Vec::new());
}
let end = start.saturating_add(*len).min(pieces.len());
Ok(pieces[*start..end]
.iter()
.map(|piece| (*piece).to_owned())
.collect())
}
Self::LeftExtract { index, len } => {
let token = select_token(tokens, *index)?;
Ok(vec![take_left(token, *len)])
}
Self::RightExtract { index, len } => {
let token = select_token(tokens, *index)?;
Ok(vec![take_right(token, *len)])
}
Self::Compose { steps } => {
let mut current = tokens.to_vec();
for step in steps {
current = step.apply_tokens(¤t)?;
}
Ok(current)
}
}
}
fn validate(&self) -> Result<(), MorphismValidationError> {
match self {
Self::RenamePrefix { from, to } if from == to => {
Err(MorphismValidationError::RenamePrefixIdentity)
}
Self::SummarizeTail { preserve_segments } if *preserve_segments == 0 => {
Err(MorphismValidationError::SummarizeTailMustPreserveSegments)
}
Self::HashPartition { buckets } if *buckets == 0 => {
Err(MorphismValidationError::HashPartitionRequiresBuckets)
}
Self::WildcardCapture { index } if *index == 0 => {
Err(MorphismValidationError::WildcardCaptureRequiresIndex)
}
Self::DeterministicHash { buckets, .. } if *buckets == 0 => {
Err(MorphismValidationError::DeterministicHashRequiresBuckets)
}
Self::DeterministicHash { source_indices, .. } if source_indices.contains(&0) => {
Err(MorphismValidationError::DeterministicHashIndexMustBePositive)
}
Self::SplitSlice { index, .. } if *index == 0 => {
Err(MorphismValidationError::SplitSliceRequiresIndex)
}
Self::SplitSlice { delimiter, .. } if delimiter.is_empty() => {
Err(MorphismValidationError::SplitSliceRequiresDelimiter)
}
Self::SplitSlice { len, .. } if *len == 0 => {
Err(MorphismValidationError::SplitSliceRequiresLength)
}
Self::LeftExtract { index, .. } if *index == 0 => {
Err(MorphismValidationError::LeftExtractRequiresIndex)
}
Self::LeftExtract { len, .. } if *len == 0 => {
Err(MorphismValidationError::LeftExtractRequiresLength)
}
Self::RightExtract { index, .. } if *index == 0 => {
Err(MorphismValidationError::RightExtractRequiresIndex)
}
Self::RightExtract { len, .. } if *len == 0 => {
Err(MorphismValidationError::RightExtractRequiresLength)
}
Self::Compose { steps } if steps.is_empty() => {
Err(MorphismValidationError::ComposeRequiresSteps)
}
Self::Compose { steps } => {
for step in steps {
step.validate()?;
}
Ok(())
}
Self::Identity
| Self::RedactLiterals
| Self::RenamePrefix { .. }
| Self::SummarizeTail { .. }
| Self::HashPartition { .. }
| Self::WildcardCapture { .. }
| Self::DeterministicHash { .. }
| Self::SplitSlice { .. }
| Self::LeftExtract { .. }
| Self::RightExtract { .. } => Ok(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuotaPolicy {
pub max_expansion_factor: u16,
pub max_fanout: u16,
pub max_observability_bytes: u32,
pub max_handoff_duration: Option<Duration>,
pub revocation_required: bool,
}
impl Default for QuotaPolicy {
fn default() -> Self {
Self {
max_expansion_factor: 1,
max_fanout: 1,
max_observability_bytes: 4_096,
max_handoff_duration: None,
revocation_required: false,
}
}
}
impl QuotaPolicy {
fn validate(&self) -> Result<(), MorphismValidationError> {
if self.max_expansion_factor == 0 {
return Err(MorphismValidationError::ZeroMaxExpansionFactor);
}
if self.max_fanout == 0 {
return Err(MorphismValidationError::ZeroMaxFanout);
}
if self.max_observability_bytes == 0 {
return Err(MorphismValidationError::ZeroMaxObservabilityBytes);
}
if self
.max_handoff_duration
.is_some_and(|duration| duration.is_zero())
{
return Err(MorphismValidationError::ZeroMaxHandoffDuration);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Morphism {
pub source_language: SubjectPattern,
pub dest_language: SubjectPattern,
pub class: MorphismClass,
pub transform: SubjectTransform,
pub reversibility: ReversibilityRequirement,
pub capability_requirements: Vec<FabricCapability>,
pub sharing_policy: SharingPolicy,
pub privacy_policy: PrivacyPolicy,
pub response_policy: ResponsePolicy,
pub quota_policy: QuotaPolicy,
pub evidence_policy: EvidencePolicy,
}
impl Default for Morphism {
fn default() -> Self {
Self {
source_language: SubjectPattern::new("fabric.subject.>"),
dest_language: SubjectPattern::new("fabric.subject.>"),
class: MorphismClass::DerivedView,
transform: SubjectTransform::Identity,
reversibility: ReversibilityRequirement::EvidenceBacked,
capability_requirements: vec![FabricCapability::RewriteNamespace],
sharing_policy: SharingPolicy::Private,
privacy_policy: PrivacyPolicy::default(),
response_policy: ResponsePolicy::PreserveCallerReplies,
quota_policy: QuotaPolicy::default(),
evidence_policy: EvidencePolicy::default(),
}
}
}
impl Morphism {
pub fn validate(&self) -> Result<(), MorphismValidationError> {
self.transform.validate()?;
self.quota_policy.validate()?;
if let Some(duplicate) = duplicate_capability(&self.capability_requirements) {
return Err(MorphismValidationError::DuplicateCapability(duplicate));
}
if self.reversibility == ReversibilityRequirement::Bijective
&& !self.transform.is_invertible()
{
return Err(MorphismValidationError::TransformCannotSatisfyBijectiveRequirement);
}
match self.class {
MorphismClass::Authoritative => {
if self.capability_requirements.is_empty() {
return Err(MorphismValidationError::AuthoritativeRequiresCapability);
}
if !self
.capability_requirements
.iter()
.copied()
.any(FabricCapability::is_authority_bearing)
{
return Err(MorphismValidationError::AuthoritativeRequiresAuthorityCapability);
}
if self.response_policy != ResponsePolicy::ReplyAuthoritative {
return Err(MorphismValidationError::AuthoritativeRequiresReplyAuthority);
}
if self.reversibility == ReversibilityRequirement::Irreversible {
return Err(MorphismValidationError::AuthoritativeMustBeReversible);
}
if self.transform.is_lossy() {
return Err(MorphismValidationError::AuthoritativeTransformMustBeLossless);
}
if let SubjectTransform::RenamePrefix { from, to } = &self.transform
&& (from.has_wildcards() || to.has_wildcards())
{
return Err(MorphismValidationError::AuthoritativeRenameMustBeLiteralOnly);
}
}
MorphismClass::DerivedView => {
if self
.capability_requirements
.iter()
.copied()
.any(FabricCapability::is_authority_bearing)
{
return Err(
MorphismValidationError::DerivedViewCannotRequireAuthorityCapability,
);
}
if self.response_policy == ResponsePolicy::ReplyAuthoritative {
return Err(MorphismValidationError::DerivedViewCannotOriginateReplyAuthority);
}
}
MorphismClass::Egress => {
if self.response_policy != ResponsePolicy::StripReplies {
return Err(MorphismValidationError::EgressMustStripReplies);
}
if self.reversibility != ReversibilityRequirement::Irreversible {
return Err(MorphismValidationError::EgressMustBeIrreversible);
}
if self.sharing_policy == SharingPolicy::Private {
return Err(MorphismValidationError::EgressMustCrossBoundary);
}
}
MorphismClass::Delegation => {
if !self
.capability_requirements
.contains(&FabricCapability::DelegateNamespace)
{
return Err(MorphismValidationError::DelegationRequiresDelegateCapability);
}
if self.reversibility == ReversibilityRequirement::Irreversible {
return Err(MorphismValidationError::DelegationMustBeReversible);
}
if self.transform.is_lossy() {
return Err(MorphismValidationError::DelegationTransformMustBeLossless);
}
if self.quota_policy.max_handoff_duration.is_none() {
return Err(MorphismValidationError::DelegationMustBeTimeBounded);
}
if !self.quota_policy.revocation_required {
return Err(MorphismValidationError::DelegationMustBeRevocable);
}
}
}
Ok(())
}
#[must_use]
pub fn authority_facet(&self) -> AuthorityFacet {
AuthorityFacet {
class: self.class,
capability_requirements: canonical_capabilities(&self.capability_requirements),
response_policy: self.response_policy,
}
}
#[must_use]
pub fn reversibility_facet(&self) -> ReversibilityFacet {
ReversibilityFacet {
requirement: self.reversibility,
lossy_transform: self.transform.is_lossy(),
}
}
#[must_use]
pub fn secrecy_facet(&self) -> SecrecyFacet {
SecrecyFacet {
sharing_policy: self.sharing_policy,
privacy_policy: self.privacy_policy.clone(),
}
}
#[must_use]
pub fn cost_facet(&self) -> CostFacet {
CostFacet {
quota_policy: self.quota_policy.clone(),
}
}
#[must_use]
pub fn observability_facet(&self) -> ObservabilityFacet {
ObservabilityFacet {
evidence_policy: self.evidence_policy.clone(),
}
}
#[must_use]
pub fn facet_set(&self) -> MorphismFacetSet {
MorphismFacetSet {
authority: self.authority_facet(),
reversibility: self.reversibility_facet(),
secrecy: self.secrecy_facet(),
cost: self.cost_facet(),
observability: self.observability_facet(),
}
}
pub fn compile(&self) -> Result<MorphismCertificate, MorphismValidationError> {
self.validate()?;
let bytes = serde_json::to_vec(self)
.map_err(|error| MorphismValidationError::SerializeCertificate(error.to_string()))?;
let mut hasher = DetHasher::default();
hasher.write(&bytes);
Ok(MorphismCertificate {
fingerprint: format!("{:016x}", hasher.finish()),
class: self.class,
source_language: self.source_language.clone(),
dest_language: self.dest_language.clone(),
transform: self.transform.clone(),
facets: self.facet_set(),
})
}
pub fn compile_export_plan(
&self,
requested_reply_space: Option<ReplySpaceRule>,
) -> Result<ExportPlan, MorphismCompileError> {
let parts =
compile_boundary_plan(self, MorphismPlanDirection::Export, requested_reply_space)?;
Ok(ExportPlan {
direction: parts.direction,
certificate: parts.certificate,
attached_capabilities: parts.attached_capabilities,
selected_reply_space: parts.selected_reply_space,
permitted_reply_spaces: parts.permitted_reply_spaces,
metadata_boundary: parts.metadata_boundary,
steps: parts.steps,
reasoning: parts.reasoning,
})
}
pub fn compile_import_plan(
&self,
requested_reply_space: Option<ReplySpaceRule>,
) -> Result<ImportPlan, MorphismCompileError> {
let parts =
compile_boundary_plan(self, MorphismPlanDirection::Import, requested_reply_space)?;
Ok(ImportPlan {
direction: parts.direction,
certificate: parts.certificate,
attached_capabilities: parts.attached_capabilities,
selected_reply_space: parts.selected_reply_space,
permitted_reply_spaces: parts.permitted_reply_spaces,
metadata_boundary: parts.metadata_boundary,
steps: parts.steps,
reasoning: parts.reasoning,
})
}
fn crosses_boundary(&self) -> bool {
self.sharing_policy != SharingPolicy::Private
|| matches!(
self.class,
MorphismClass::Egress | MorphismClass::Delegation
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityFacet {
pub class: MorphismClass,
pub capability_requirements: Vec<FabricCapability>,
pub response_policy: ResponsePolicy,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReversibilityFacet {
pub requirement: ReversibilityRequirement,
pub lossy_transform: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SecrecyFacet {
pub sharing_policy: SharingPolicy,
pub privacy_policy: PrivacyPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CostFacet {
pub quota_policy: QuotaPolicy,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ObservabilityFacet {
pub evidence_policy: EvidencePolicy,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MorphismFacetSet {
pub authority: AuthorityFacet,
pub reversibility: ReversibilityFacet,
pub secrecy: SecrecyFacet,
pub cost: CostFacet,
pub observability: ObservabilityFacet,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MorphismCertificate {
pub fingerprint: String,
pub class: MorphismClass,
pub source_language: SubjectPattern,
pub dest_language: SubjectPattern,
pub transform: SubjectTransform,
pub facets: MorphismFacetSet,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum MorphismPlanDirection {
Import,
#[default]
Export,
}
impl MorphismPlanDirection {
#[must_use]
const fn as_str(self) -> &'static str {
match self {
Self::Import => "import",
Self::Export => "export",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MorphismPlanStep {
MatchSourceLanguage,
EnforceCapabilityEnvelope,
ApplyTransformCertificate,
EnforceReplySpace,
EnforceMetadataBoundary,
EmitAuditReasoning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticCycleClass {
Authority,
Reversibility,
Capability,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetadataBoundarySummary {
pub crosses_boundary: bool,
pub metadata_disclosure: MetadataDisclosure,
pub subject_literals_redacted: bool,
pub cross_tenant_flow_allowed: bool,
pub payload_hashes_recorded: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MorphismAuditNote {
pub code: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExportPlan {
pub direction: MorphismPlanDirection,
pub certificate: MorphismCertificate,
pub attached_capabilities: Vec<FabricCapability>,
pub selected_reply_space: Option<ReplySpaceRule>,
pub permitted_reply_spaces: Vec<ReplySpaceRule>,
pub metadata_boundary: MetadataBoundarySummary,
pub steps: Vec<MorphismPlanStep>,
pub reasoning: Vec<MorphismAuditNote>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImportPlan {
pub direction: MorphismPlanDirection,
pub certificate: MorphismCertificate,
pub attached_capabilities: Vec<FabricCapability>,
pub selected_reply_space: Option<ReplySpaceRule>,
pub permitted_reply_spaces: Vec<ReplySpaceRule>,
pub metadata_boundary: MetadataBoundarySummary,
pub steps: Vec<MorphismPlanStep>,
pub reasoning: Vec<MorphismAuditNote>,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MorphismCompileError {
#[error(transparent)]
InvalidMorphism(#[from] MorphismValidationError),
#[error(
"cross-boundary reply space `{requested:?}` is not permitted for response policy `{policy:?}`; permitted reply spaces: {permitted:?}"
)]
ReplySpaceNotPermitted {
policy: ResponsePolicy,
requested: ReplySpaceRule,
permitted: Vec<ReplySpaceRule>,
},
#[error("cross-boundary replies are forbidden for response policy `{policy:?}`")]
ReplySpaceForbidden {
policy: ResponsePolicy,
},
#[error("semantic {class:?} cycle detected across morphism chain {path:?}")]
SemanticCycleDetected {
class: SemanticCycleClass,
path: Vec<String>,
},
#[error(
"sharing policy `{sharing_policy:?}` with metadata disclosure `{metadata_disclosure:?}` requires explicit cross-tenant permission"
)]
MetadataBoundaryViolation {
sharing_policy: SharingPolicy,
metadata_disclosure: MetadataDisclosure,
},
}
pub fn detect_semantic_cycles(morphisms: &[Morphism]) -> Result<(), MorphismCompileError> {
for morphism in morphisms {
morphism.validate()?;
}
for start in 0..morphisms.len() {
let mut path = vec![start];
let mut visited = BTreeSet::from([start]);
detect_semantic_cycle_from(morphisms, start, start, &mut path, &mut visited)?;
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MorphismValidationError {
#[error("authoritative morphisms require at least one capability")]
AuthoritativeRequiresCapability,
#[error("authoritative morphisms require an authority-bearing capability")]
AuthoritativeRequiresAuthorityCapability,
#[error("authoritative morphisms must use reply-authoritative response policy")]
AuthoritativeRequiresReplyAuthority,
#[error("authoritative morphisms must be reversible")]
AuthoritativeMustBeReversible,
#[error("authoritative morphisms must use lossless transforms")]
AuthoritativeTransformMustBeLossless,
#[error("authoritative rename-prefix morphisms must use literal-only patterns")]
AuthoritativeRenameMustBeLiteralOnly,
#[error("derived-view morphisms must not require authority-bearing capabilities")]
DerivedViewCannotRequireAuthorityCapability,
#[error("derived-view morphisms must not originate reply authority")]
DerivedViewCannotOriginateReplyAuthority,
#[error("egress morphisms must strip replies")]
EgressMustStripReplies,
#[error("egress morphisms must be irreversible")]
EgressMustBeIrreversible,
#[error("egress morphisms must cross a non-private sharing boundary")]
EgressMustCrossBoundary,
#[error("delegation morphisms require delegate-namespace capability")]
DelegationRequiresDelegateCapability,
#[error("delegation morphisms must be reversible")]
DelegationMustBeReversible,
#[error("delegation morphisms must use lossless transforms")]
DelegationTransformMustBeLossless,
#[error("delegation morphisms must declare a bounded handoff duration")]
DelegationMustBeTimeBounded,
#[error("delegation morphisms must be revocable")]
DelegationMustBeRevocable,
#[error("duplicate capability requirement `{0:?}`")]
DuplicateCapability(FabricCapability),
#[error("rename-prefix transform must change the namespace")]
RenamePrefixIdentity,
#[error("summarize-tail transform must preserve at least one segment")]
SummarizeTailMustPreserveSegments,
#[error("hash-partition transform requires at least one bucket")]
HashPartitionRequiresBuckets,
#[error("wildcard-capture transform requires a positive index")]
WildcardCaptureRequiresIndex,
#[error("deterministic-hash transform requires at least one bucket")]
DeterministicHashRequiresBuckets,
#[error("deterministic-hash source indices must be positive")]
DeterministicHashIndexMustBePositive,
#[error("split-slice transform requires a positive token index")]
SplitSliceRequiresIndex,
#[error("split-slice transform requires a non-empty delimiter")]
SplitSliceRequiresDelimiter,
#[error("split-slice transform requires a positive slice length")]
SplitSliceRequiresLength,
#[error("left-extract transform requires a positive token index")]
LeftExtractRequiresIndex,
#[error("left-extract transform requires a positive length")]
LeftExtractRequiresLength,
#[error("right-extract transform requires a positive token index")]
RightExtractRequiresIndex,
#[error("right-extract transform requires a positive length")]
RightExtractRequiresLength,
#[error("compose transform requires at least one step")]
ComposeRequiresSteps,
#[error("bijective reversibility requires an invertible transform")]
TransformCannotSatisfyBijectiveRequirement,
#[error("quota max expansion factor must be greater than zero")]
ZeroMaxExpansionFactor,
#[error("quota max fanout must be greater than zero")]
ZeroMaxFanout,
#[error("quota max observability bytes must be greater than zero")]
ZeroMaxObservabilityBytes,
#[error("quota max handoff duration must be greater than zero")]
ZeroMaxHandoffDuration,
#[error("failed to serialize morphism certificate: {0}")]
SerializeCertificate(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MorphismEvaluationError {
#[error("token index {index} is out of range for {available} available tokens")]
TokenIndexOutOfRange {
index: usize,
available: usize,
},
#[error("pattern `{0}` must contain only literal segments for evaluation")]
NonLiteralPattern(String),
#[error("deterministic bucket count must be greater than zero")]
ZeroBuckets,
}
fn canonical_capabilities(capabilities: &[FabricCapability]) -> Vec<FabricCapability> {
capabilities
.iter()
.copied()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn duplicate_capability(capabilities: &[FabricCapability]) -> Option<FabricCapability> {
let mut seen = BTreeSet::new();
for capability in capabilities {
if !seen.insert(*capability) {
return Some(*capability);
}
}
None
}
fn literal_only_segments(pattern: &SubjectPattern) -> Result<Vec<String>, MorphismEvaluationError> {
pattern
.segments()
.iter()
.map(|segment| match segment {
super::subject::SubjectToken::Literal(value) => Ok(value.clone()),
super::subject::SubjectToken::One | super::subject::SubjectToken::Tail => Err(
MorphismEvaluationError::NonLiteralPattern(pattern.canonical_key()),
),
})
.collect()
}
fn select_token(tokens: &[String], index: usize) -> Result<&str, MorphismEvaluationError> {
let offset = index
.checked_sub(1)
.ok_or(MorphismEvaluationError::TokenIndexOutOfRange {
index,
available: tokens.len(),
})?;
tokens
.get(offset)
.map(String::as_str)
.ok_or(MorphismEvaluationError::TokenIndexOutOfRange {
index,
available: tokens.len(),
})
}
fn deterministic_bucket(
tokens: &[String],
source_indices: &[usize],
buckets: u16,
) -> Result<u16, MorphismEvaluationError> {
if buckets == 0 {
return Err(MorphismEvaluationError::ZeroBuckets);
}
let mut hasher = DetHasher::default();
if source_indices.is_empty() {
for token in tokens {
hasher.write(token.as_bytes());
hasher.write_u8(0xff);
}
} else {
for index in source_indices {
hasher.write(select_token(tokens, *index)?.as_bytes());
hasher.write_u8(0xff);
}
}
Ok((hasher.finish() % u64::from(buckets)) as u16)
}
fn take_left(token: &str, len: usize) -> String {
let limit = token.chars().count().min(len);
token.chars().take(limit).collect()
}
fn take_right(token: &str, len: usize) -> String {
let char_count = token.chars().count();
let start = char_count.saturating_sub(len);
token.chars().skip(start).collect()
}
#[derive(Debug)]
struct CompiledPlanParts {
direction: MorphismPlanDirection,
certificate: MorphismCertificate,
attached_capabilities: Vec<FabricCapability>,
selected_reply_space: Option<ReplySpaceRule>,
permitted_reply_spaces: Vec<ReplySpaceRule>,
metadata_boundary: MetadataBoundarySummary,
steps: Vec<MorphismPlanStep>,
reasoning: Vec<MorphismAuditNote>,
}
fn compile_boundary_plan(
morphism: &Morphism,
direction: MorphismPlanDirection,
requested_reply_space: Option<ReplySpaceRule>,
) -> Result<CompiledPlanParts, MorphismCompileError> {
let certificate = morphism.compile()?;
let attached_capabilities = canonical_capabilities(&morphism.capability_requirements);
let permitted_reply_spaces = permitted_reply_spaces(morphism);
let selected_reply_space =
select_reply_space(morphism, requested_reply_space, &permitted_reply_spaces)?;
let metadata_boundary = compile_metadata_boundary(morphism)?;
let reasoning = vec![
audit_note(
"direction",
format!(
"{} plan rewrites `{}` into `{}`",
direction.as_str(),
morphism.source_language,
morphism.dest_language
),
),
audit_note(
"capabilities",
format!("attached capabilities: {attached_capabilities:?}"),
),
audit_note(
"reply_space",
format!(
"response policy {:?} selected {:?} from permitted spaces {:?}",
morphism.response_policy, selected_reply_space, permitted_reply_spaces
),
),
audit_note(
"metadata_boundary",
format!(
"boundary crosses={}, disclosure={:?}, subject_literals_redacted={}, cross_tenant_flow_allowed={}, payload_hashes_recorded={}",
metadata_boundary.crosses_boundary,
metadata_boundary.metadata_disclosure,
metadata_boundary.subject_literals_redacted,
metadata_boundary.cross_tenant_flow_allowed,
metadata_boundary.payload_hashes_recorded,
),
),
];
Ok(CompiledPlanParts {
direction,
certificate,
attached_capabilities,
selected_reply_space,
permitted_reply_spaces,
metadata_boundary,
steps: vec![
MorphismPlanStep::MatchSourceLanguage,
MorphismPlanStep::EnforceCapabilityEnvelope,
MorphismPlanStep::ApplyTransformCertificate,
MorphismPlanStep::EnforceReplySpace,
MorphismPlanStep::EnforceMetadataBoundary,
MorphismPlanStep::EmitAuditReasoning,
],
reasoning,
})
}
fn permitted_reply_spaces(morphism: &Morphism) -> Vec<ReplySpaceRule> {
match morphism.response_policy {
ResponsePolicy::StripReplies => Vec::new(),
ResponsePolicy::PreserveCallerReplies | ResponsePolicy::ForwardOpaque => {
vec![ReplySpaceRule::CallerInbox]
}
ResponsePolicy::ReplyAuthoritative => {
let prefix = morphism.dest_language.as_str().to_owned();
vec![
ReplySpaceRule::DedicatedPrefix {
prefix: prefix.clone(),
},
ReplySpaceRule::SharedPrefix { prefix },
]
}
}
}
fn select_reply_space(
morphism: &Morphism,
requested_reply_space: Option<ReplySpaceRule>,
permitted_reply_spaces: &[ReplySpaceRule],
) -> Result<Option<ReplySpaceRule>, MorphismCompileError> {
let selected_reply_space = requested_reply_space.map_or_else(
|| default_reply_space(morphism, permitted_reply_spaces),
Some,
);
if !morphism.crosses_boundary() {
return Ok(selected_reply_space);
}
selected_reply_space.map_or(Ok(None), |reply_space| {
if permitted_reply_spaces.contains(&reply_space) {
Ok(Some(reply_space))
} else if permitted_reply_spaces.is_empty() {
Err(MorphismCompileError::ReplySpaceForbidden {
policy: morphism.response_policy,
})
} else {
Err(MorphismCompileError::ReplySpaceNotPermitted {
policy: morphism.response_policy,
requested: reply_space,
permitted: permitted_reply_spaces.to_vec(),
})
}
})
}
fn default_reply_space(
morphism: &Morphism,
permitted_reply_spaces: &[ReplySpaceRule],
) -> Option<ReplySpaceRule> {
match morphism.response_policy {
ResponsePolicy::StripReplies => None,
ResponsePolicy::PreserveCallerReplies | ResponsePolicy::ForwardOpaque => {
Some(ReplySpaceRule::CallerInbox)
}
ResponsePolicy::ReplyAuthoritative => permitted_reply_spaces.first().cloned(),
}
}
fn compile_metadata_boundary(
morphism: &Morphism,
) -> Result<MetadataBoundarySummary, MorphismCompileError> {
let summary = MetadataBoundarySummary {
crosses_boundary: morphism.crosses_boundary(),
metadata_disclosure: morphism.privacy_policy.metadata_disclosure,
subject_literals_redacted: morphism.privacy_policy.redact_subject_literals
|| matches!(morphism.transform, SubjectTransform::RedactLiterals),
cross_tenant_flow_allowed: morphism.privacy_policy.allow_cross_tenant_flow,
payload_hashes_recorded: morphism.evidence_policy.record_payload_hashes,
};
if summary.crosses_boundary
&& matches!(
morphism.sharing_policy,
SharingPolicy::Federated | SharingPolicy::PublicRead
)
&& summary.metadata_disclosure == MetadataDisclosure::Full
&& !summary.cross_tenant_flow_allowed
{
return Err(MorphismCompileError::MetadataBoundaryViolation {
sharing_policy: morphism.sharing_policy,
metadata_disclosure: summary.metadata_disclosure,
});
}
Ok(summary)
}
fn detect_semantic_cycle_from(
morphisms: &[Morphism],
start: usize,
current: usize,
path: &mut Vec<usize>,
visited: &mut BTreeSet<usize>,
) -> Result<(), MorphismCompileError> {
if morphisms[current]
.dest_language
.overlaps(&morphisms[start].source_language)
{
let cycle = path
.iter()
.map(|index| &morphisms[*index])
.collect::<Vec<_>>();
if let Some(class) = classify_semantic_cycle(&cycle) {
return Err(MorphismCompileError::SemanticCycleDetected {
class,
path: path
.iter()
.map(|index| describe_morphism(&morphisms[*index]))
.collect(),
});
}
}
for next in 0..morphisms.len() {
if !morphisms[current]
.dest_language
.overlaps(&morphisms[next].source_language)
{
continue;
}
if visited.contains(&next) {
continue;
}
visited.insert(next);
path.push(next);
let result = detect_semantic_cycle_from(morphisms, start, next, path, visited);
path.pop();
visited.remove(&next);
result?;
}
Ok(())
}
fn classify_semantic_cycle(morphisms: &[&Morphism]) -> Option<SemanticCycleClass> {
if morphisms.iter().any(|morphism| {
morphism.class == MorphismClass::Authoritative
|| morphism.response_policy == ResponsePolicy::ReplyAuthoritative
|| morphism
.capability_requirements
.iter()
.copied()
.any(FabricCapability::is_authority_bearing)
}) {
return Some(SemanticCycleClass::Authority);
}
if morphisms.iter().any(|morphism| {
morphism.reversibility == ReversibilityRequirement::Irreversible
|| morphism.transform.is_lossy()
}) {
return Some(SemanticCycleClass::Reversibility);
}
if morphisms.iter().any(|morphism| {
morphism.sharing_policy != SharingPolicy::Private
|| morphism.capability_requirements.iter().any(|capability| {
matches!(
capability,
FabricCapability::ObserveEvidence | FabricCapability::CrossBoundaryEgress
)
})
}) {
return Some(SemanticCycleClass::Capability);
}
None
}
fn describe_morphism(morphism: &Morphism) -> String {
format!(
"{:?}:{}->{}",
morphism.class, morphism.source_language, morphism.dest_language
)
}
fn audit_note(code: &str, detail: String) -> MorphismAuditNote {
MorphismAuditNote {
code: code.to_owned(),
detail,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn authoritative_morphism() -> Morphism {
Morphism {
source_language: SubjectPattern::new("tenant.orders"),
dest_language: SubjectPattern::new("authority.orders"),
class: MorphismClass::Authoritative,
transform: SubjectTransform::RenamePrefix {
from: SubjectPattern::new("tenant.orders"),
to: SubjectPattern::new("authority.orders"),
},
reversibility: ReversibilityRequirement::Bijective,
capability_requirements: vec![
FabricCapability::CarryAuthority,
FabricCapability::ReplyAuthority,
],
response_policy: ResponsePolicy::ReplyAuthoritative,
..Morphism::default()
}
}
#[test]
fn authoritative_compile_produces_deterministic_certificate() {
let morphism = authoritative_morphism();
let first = morphism.compile().expect("compile certificate");
let second = morphism.compile().expect("compile certificate twice");
assert_eq!(first, second);
assert_eq!(first.class, MorphismClass::Authoritative);
assert_eq!(
first.facets.authority.capability_requirements,
vec![
FabricCapability::CarryAuthority,
FabricCapability::ReplyAuthority,
]
);
}
#[test]
fn authoritative_morphisms_reject_lossy_or_wildcard_rewrites() {
let mut lossy = authoritative_morphism();
lossy.reversibility = ReversibilityRequirement::EvidenceBacked;
lossy.transform = SubjectTransform::RedactLiterals;
assert_eq!(
lossy.validate(),
Err(MorphismValidationError::AuthoritativeTransformMustBeLossless)
);
let mut wildcard = authoritative_morphism();
wildcard.transform = SubjectTransform::RenamePrefix {
from: SubjectPattern::new("tenant.*"),
to: SubjectPattern::new("authority.orders"),
};
assert_eq!(
wildcard.validate(),
Err(MorphismValidationError::AuthoritativeRenameMustBeLiteralOnly)
);
}
#[test]
fn delegation_requires_delegate_capability_bounded_duration_and_revocation() {
let mut delegation = Morphism {
class: MorphismClass::Delegation,
response_policy: ResponsePolicy::ForwardOpaque,
..Morphism::default()
};
assert_eq!(
delegation.validate(),
Err(MorphismValidationError::DelegationRequiresDelegateCapability)
);
delegation.capability_requirements = vec![FabricCapability::DelegateNamespace];
assert_eq!(
delegation.validate(),
Err(MorphismValidationError::DelegationMustBeTimeBounded)
);
delegation.quota_policy.max_handoff_duration = Some(Duration::from_secs(30));
assert_eq!(
delegation.validate(),
Err(MorphismValidationError::DelegationMustBeRevocable)
);
delegation.quota_policy.revocation_required = true;
assert!(delegation.validate().is_ok());
}
#[test]
fn delegation_rejects_irreversible_and_lossy_transforms() {
let mut delegation = Morphism {
source_language: SubjectPattern::new("tenant.rpc"),
dest_language: SubjectPattern::new("delegate.rpc"),
class: MorphismClass::Delegation,
capability_requirements: vec![FabricCapability::DelegateNamespace],
response_policy: ResponsePolicy::ForwardOpaque,
sharing_policy: SharingPolicy::TenantScoped,
..Morphism::default()
};
delegation.quota_policy.max_handoff_duration = Some(Duration::from_secs(30));
delegation.quota_policy.revocation_required = true;
delegation.reversibility = ReversibilityRequirement::Irreversible;
assert_eq!(
delegation.validate(),
Err(MorphismValidationError::DelegationMustBeReversible)
);
delegation.reversibility = ReversibilityRequirement::EvidenceBacked;
delegation.transform = SubjectTransform::RedactLiterals;
assert_eq!(
delegation.validate(),
Err(MorphismValidationError::DelegationTransformMustBeLossless)
);
}
#[test]
fn egress_requires_stripped_replies_and_one_way_reversibility() {
let mut egress = Morphism {
class: MorphismClass::Egress,
sharing_policy: SharingPolicy::Federated,
reversibility: ReversibilityRequirement::Irreversible,
..Morphism::default()
};
assert_eq!(
egress.validate(),
Err(MorphismValidationError::EgressMustStripReplies)
);
egress.response_policy = ResponsePolicy::StripReplies;
egress.reversibility = ReversibilityRequirement::EvidenceBacked;
assert_eq!(
egress.validate(),
Err(MorphismValidationError::EgressMustBeIrreversible)
);
egress.reversibility = ReversibilityRequirement::Irreversible;
egress.sharing_policy = SharingPolicy::Private;
assert_eq!(
egress.validate(),
Err(MorphismValidationError::EgressMustCrossBoundary)
);
}
#[test]
fn facet_views_change_independently() {
let base = Morphism::default();
let mut cost_variant = base.clone();
cost_variant.quota_policy.max_fanout = 8;
assert_eq!(base.authority_facet(), cost_variant.authority_facet());
assert_eq!(
base.reversibility_facet(),
cost_variant.reversibility_facet()
);
assert_eq!(base.secrecy_facet(), cost_variant.secrecy_facet());
assert_ne!(base.cost_facet(), cost_variant.cost_facet());
assert_eq!(
base.observability_facet(),
cost_variant.observability_facet()
);
let mut observability_variant = base.clone();
observability_variant
.evidence_policy
.record_counterfactual_branches = true;
assert_eq!(
base.authority_facet(),
observability_variant.authority_facet()
);
assert_eq!(base.cost_facet(), observability_variant.cost_facet());
assert_ne!(
base.observability_facet(),
observability_variant.observability_facet()
);
}
#[test]
fn duplicate_capabilities_fail_closed() {
let mut morphism = authoritative_morphism();
morphism.capability_requirements = vec![
FabricCapability::ReplyAuthority,
FabricCapability::CarryAuthority,
FabricCapability::ReplyAuthority,
];
assert_eq!(
morphism.validate(),
Err(MorphismValidationError::DuplicateCapability(
FabricCapability::ReplyAuthority
))
);
}
#[test]
fn derived_views_cannot_smuggle_authority_or_reply_rebinding() {
let mut derived_view = Morphism::default();
derived_view.capability_requirements = vec![FabricCapability::CarryAuthority];
assert_eq!(
derived_view.validate(),
Err(MorphismValidationError::DerivedViewCannotRequireAuthorityCapability)
);
derived_view.capability_requirements = vec![FabricCapability::RewriteNamespace];
derived_view.response_policy = ResponsePolicy::ReplyAuthoritative;
assert_eq!(
derived_view.validate(),
Err(MorphismValidationError::DerivedViewCannotOriginateReplyAuthority)
);
}
#[test]
fn wildcard_capture_and_compose_apply_deterministically() {
let tokens = vec![
String::from("tenant"),
String::from("orders-eu"),
String::from("priority"),
];
let transform = SubjectTransform::Compose {
steps: vec![
SubjectTransform::WildcardCapture { index: 2 },
SubjectTransform::SplitSlice {
index: 1,
delimiter: String::from("-"),
start: 0,
len: 1,
},
],
};
assert_eq!(
transform
.apply_tokens(&tokens)
.expect("compose should evaluate"),
vec![String::from("orders")]
);
assert!(!transform.is_invertible());
}
#[test]
fn rename_prefix_and_remaining_lossy_variants_cover_expected_behavior() {
let tokens = vec![
String::from("tenant"),
String::from("orders"),
String::from("priority"),
];
let rename = SubjectTransform::RenamePrefix {
from: SubjectPattern::new("tenant.orders"),
to: SubjectPattern::new("authority.orders"),
};
let rewritten = rename
.apply_tokens(&tokens)
.expect("rename-prefix should rewrite matching literal prefixes");
assert_eq!(
rewritten,
vec![
String::from("authority"),
String::from("orders"),
String::from("priority"),
]
);
assert_eq!(
rename
.inverse()
.expect("literal rename should be invertible")
.apply_tokens(&rewritten)
.expect("inverse rename should roundtrip"),
tokens
);
let redacted = SubjectTransform::RedactLiterals;
assert_eq!(
redacted
.apply_tokens(&rewritten)
.expect("redaction should preserve token count"),
vec![String::from("_"), String::from("_"), String::from("_"),]
);
assert!(redacted.is_lossy());
let summarized = SubjectTransform::SummarizeTail {
preserve_segments: 2,
};
assert_eq!(
summarized
.apply_tokens(&rewritten)
.expect("tail summary should preserve requested prefix"),
vec![
String::from("authority"),
String::from("orders"),
String::from("..."),
]
);
assert!(summarized.is_lossy());
let partition = SubjectTransform::HashPartition { buckets: 8 };
let first = partition
.apply_tokens(&rewritten)
.expect("hash partition should evaluate deterministically");
let second = partition
.apply_tokens(&rewritten)
.expect("hash partition should remain stable");
assert_eq!(first, second);
let bucket = first[0]
.parse::<u16>()
.expect("hash partition must emit a bucket number");
assert!(bucket < 8);
}
#[test]
fn deterministic_hash_is_stable_for_selected_tokens() {
let tokens = vec![
String::from("tenant"),
String::from("region"),
String::from("user"),
];
let transform = SubjectTransform::DeterministicHash {
buckets: 32,
source_indices: vec![1, 3],
};
let first = transform
.apply_tokens(&tokens)
.expect("hash should evaluate deterministically");
let second = transform
.apply_tokens(&tokens)
.expect("hash should evaluate deterministically twice");
assert_eq!(first, second);
}
#[test]
fn transform_validation_rejects_invalid_core_parameters() {
assert_eq!(
SubjectTransform::SummarizeTail {
preserve_segments: 0,
}
.validate(),
Err(MorphismValidationError::SummarizeTailMustPreserveSegments)
);
assert_eq!(
SubjectTransform::HashPartition { buckets: 0 }.validate(),
Err(MorphismValidationError::HashPartitionRequiresBuckets)
);
assert_eq!(
SubjectTransform::Compose { steps: Vec::new() }.validate(),
Err(MorphismValidationError::ComposeRequiresSteps)
);
}
#[test]
fn left_and_right_extract_project_expected_substrings() {
let tokens = vec![String::from("priority")];
assert_eq!(
SubjectTransform::LeftExtract { index: 1, len: 4 }
.apply_tokens(&tokens)
.expect("left extract should evaluate"),
vec![String::from("prio")]
);
assert_eq!(
SubjectTransform::RightExtract { index: 1, len: 4 }
.apply_tokens(&tokens)
.expect("right extract should evaluate"),
vec![String::from("rity")]
);
}
#[test]
fn bijective_reversibility_rejects_irreversible_transforms() {
let mut morphism = authoritative_morphism();
morphism.transform = SubjectTransform::DeterministicHash {
buckets: 16,
source_indices: vec![1],
};
assert_eq!(
morphism.validate(),
Err(MorphismValidationError::TransformCannotSatisfyBijectiveRequirement)
);
}
#[test]
fn reversible_compose_builds_inverse_in_reverse_order() {
let transform = SubjectTransform::Compose {
steps: vec![
SubjectTransform::RenamePrefix {
from: SubjectPattern::new("tenant.orders"),
to: SubjectPattern::new("authority.orders"),
},
SubjectTransform::Identity,
],
};
let inverse = transform.inverse().expect("compose should be invertible");
assert_eq!(
inverse,
SubjectTransform::Compose {
steps: vec![
SubjectTransform::Identity,
SubjectTransform::RenamePrefix {
from: SubjectPattern::new("authority.orders"),
to: SubjectPattern::new("tenant.orders"),
},
],
}
);
}
#[test]
fn export_plan_compilation_attaches_certificate_capabilities_and_reply_space() {
let mut morphism = authoritative_morphism();
morphism.sharing_policy = SharingPolicy::Federated;
morphism.privacy_policy.allow_cross_tenant_flow = true;
let plan = morphism
.compile_export_plan(None)
.expect("export plan should compile");
assert_eq!(plan.direction, MorphismPlanDirection::Export);
assert_eq!(
plan.attached_capabilities,
vec![
FabricCapability::CarryAuthority,
FabricCapability::ReplyAuthority,
]
);
assert_eq!(
plan.selected_reply_space,
Some(ReplySpaceRule::DedicatedPrefix {
prefix: String::from("authority.orders"),
})
);
assert!(
plan.permitted_reply_spaces
.contains(&ReplySpaceRule::SharedPrefix {
prefix: String::from("authority.orders"),
})
);
assert!(plan.reasoning.iter().any(|note| note.code == "reply_space"));
}
#[test]
fn import_plan_compilation_defaults_forward_opaque_to_caller_inbox() {
let mut delegation = Morphism {
source_language: SubjectPattern::new("tenant.rpc"),
dest_language: SubjectPattern::new("delegate.rpc"),
class: MorphismClass::Delegation,
capability_requirements: vec![FabricCapability::DelegateNamespace],
response_policy: ResponsePolicy::ForwardOpaque,
sharing_policy: SharingPolicy::TenantScoped,
..Morphism::default()
};
delegation.quota_policy.max_handoff_duration = Some(Duration::from_secs(30));
delegation.quota_policy.revocation_required = true;
delegation.privacy_policy.allow_cross_tenant_flow = true;
let plan = delegation
.compile_import_plan(None)
.expect("import plan should compile");
assert_eq!(plan.direction, MorphismPlanDirection::Import);
assert_eq!(plan.selected_reply_space, Some(ReplySpaceRule::CallerInbox));
assert_eq!(
plan.permitted_reply_spaces,
vec![ReplySpaceRule::CallerInbox]
);
}
#[test]
fn cross_boundary_reply_space_enforcement_rejects_unpermitted_reply_prefixes() {
let mut morphism = Morphism {
source_language: SubjectPattern::new("tenant.requests"),
dest_language: SubjectPattern::new("federated.requests"),
sharing_policy: SharingPolicy::Federated,
..Morphism::default()
};
morphism.privacy_policy.allow_cross_tenant_flow = true;
let err = morphism
.compile_export_plan(Some(ReplySpaceRule::DedicatedPrefix {
prefix: String::from("reply.bad"),
}))
.expect_err("non-caller reply prefix should be rejected");
assert_eq!(
err,
MorphismCompileError::ReplySpaceNotPermitted {
policy: ResponsePolicy::PreserveCallerReplies,
requested: ReplySpaceRule::DedicatedPrefix {
prefix: String::from("reply.bad"),
},
permitted: vec![ReplySpaceRule::CallerInbox],
}
);
}
#[test]
fn cross_boundary_strip_replies_forbids_any_selected_reply_space() {
let mut egress = Morphism {
source_language: SubjectPattern::new("tenant.audit"),
dest_language: SubjectPattern::new("federated.audit"),
class: MorphismClass::Egress,
transform: SubjectTransform::RedactLiterals,
reversibility: ReversibilityRequirement::Irreversible,
sharing_policy: SharingPolicy::Federated,
response_policy: ResponsePolicy::StripReplies,
..Morphism::default()
};
egress.privacy_policy.allow_cross_tenant_flow = true;
let err = egress
.compile_export_plan(Some(ReplySpaceRule::CallerInbox))
.expect_err("strip-replies egress should reject every reply space");
assert_eq!(
err,
MorphismCompileError::ReplySpaceForbidden {
policy: ResponsePolicy::StripReplies,
}
);
}
#[test]
fn metadata_boundary_checks_fail_closed_for_public_full_disclosure() {
let mut morphism = Morphism {
source_language: SubjectPattern::new("tenant.audit"),
dest_language: SubjectPattern::new("public.audit"),
sharing_policy: SharingPolicy::PublicRead,
..Morphism::default()
};
morphism.privacy_policy.metadata_disclosure = MetadataDisclosure::Full;
let err = morphism
.compile_export_plan(None)
.expect_err("public full disclosure should require explicit cross-tenant permission");
assert_eq!(
err,
MorphismCompileError::MetadataBoundaryViolation {
sharing_policy: SharingPolicy::PublicRead,
metadata_disclosure: MetadataDisclosure::Full,
}
);
}
#[test]
fn metadata_boundary_summary_records_redaction_and_payload_hash_evidence() {
let mut morphism = Morphism {
source_language: SubjectPattern::new("tenant.audit"),
dest_language: SubjectPattern::new("federated.audit"),
transform: SubjectTransform::RedactLiterals,
sharing_policy: SharingPolicy::Federated,
..Morphism::default()
};
morphism.privacy_policy.allow_cross_tenant_flow = true;
morphism.evidence_policy.record_payload_hashes = true;
let plan = morphism
.compile_export_plan(None)
.expect("cross-boundary plan should compile");
assert_eq!(
plan.metadata_boundary,
MetadataBoundarySummary {
crosses_boundary: true,
metadata_disclosure: MetadataDisclosure::Hashed,
subject_literals_redacted: true,
cross_tenant_flow_allowed: true,
payload_hashes_recorded: true,
}
);
assert!(plan.reasoning.iter().any(|note| {
note.code == "metadata_boundary"
&& note.detail.contains("subject_literals_redacted=true")
&& note.detail.contains("payload_hashes_recorded=true")
}));
}
#[test]
fn semantic_cycle_detection_flags_authority_cycles() {
let forward = authoritative_morphism();
let reverse = Morphism {
source_language: SubjectPattern::new("authority.orders"),
dest_language: SubjectPattern::new("tenant.orders"),
class: MorphismClass::Authoritative,
transform: SubjectTransform::RenamePrefix {
from: SubjectPattern::new("authority.orders"),
to: SubjectPattern::new("tenant.orders"),
},
reversibility: ReversibilityRequirement::Bijective,
capability_requirements: vec![
FabricCapability::CarryAuthority,
FabricCapability::ReplyAuthority,
],
response_policy: ResponsePolicy::ReplyAuthoritative,
..Morphism::default()
};
let err = detect_semantic_cycles(&[forward, reverse])
.expect_err("authority-bearing cycle should be rejected");
assert!(matches!(
err,
MorphismCompileError::SemanticCycleDetected {
class: SemanticCycleClass::Authority,
..
}
));
}
#[test]
fn semantic_cycle_detection_flags_irreversible_cycles() {
let irreversible = Morphism {
source_language: SubjectPattern::new("tenant.audit"),
dest_language: SubjectPattern::new("egress.audit"),
class: MorphismClass::Egress,
transform: SubjectTransform::RedactLiterals,
reversibility: ReversibilityRequirement::Irreversible,
sharing_policy: SharingPolicy::Federated,
response_policy: ResponsePolicy::StripReplies,
..Morphism::default()
};
let reverse = Morphism {
source_language: SubjectPattern::new("egress.audit"),
dest_language: SubjectPattern::new("tenant.audit"),
..Morphism::default()
};
let err = detect_semantic_cycles(&[irreversible, reverse])
.expect_err("irreversible cycle should be rejected");
assert!(matches!(
err,
MorphismCompileError::SemanticCycleDetected {
class: SemanticCycleClass::Reversibility,
..
}
));
}
#[test]
fn semantic_cycle_detection_flags_capability_cycles() {
let mut boundary = Morphism {
source_language: SubjectPattern::new("tenant.stream"),
dest_language: SubjectPattern::new("federated.stream"),
capability_requirements: vec![
FabricCapability::RewriteNamespace,
FabricCapability::CrossBoundaryEgress,
],
sharing_policy: SharingPolicy::Federated,
..Morphism::default()
};
boundary.privacy_policy.allow_cross_tenant_flow = true;
let reverse = Morphism {
source_language: SubjectPattern::new("federated.stream"),
dest_language: SubjectPattern::new("tenant.stream"),
..Morphism::default()
};
let err = detect_semantic_cycles(&[boundary, reverse])
.expect_err("boundary capability cycle should be rejected");
assert!(matches!(
err,
MorphismCompileError::SemanticCycleDetected {
class: SemanticCycleClass::Capability,
..
}
));
}
#[test]
fn semantic_cycle_detection_flags_multi_hop_boundary_cycles() {
let mut first = Morphism {
source_language: SubjectPattern::new("tenant.stream"),
dest_language: SubjectPattern::new("federated.stream"),
capability_requirements: vec![
FabricCapability::RewriteNamespace,
FabricCapability::CrossBoundaryEgress,
],
sharing_policy: SharingPolicy::Federated,
..Morphism::default()
};
first.privacy_policy.allow_cross_tenant_flow = true;
let mut second = Morphism {
source_language: SubjectPattern::new("federated.stream"),
dest_language: SubjectPattern::new("shared.stream"),
capability_requirements: vec![FabricCapability::ObserveEvidence],
sharing_policy: SharingPolicy::Federated,
..Morphism::default()
};
second.privacy_policy.allow_cross_tenant_flow = true;
let third = Morphism {
source_language: SubjectPattern::new("shared.stream"),
dest_language: SubjectPattern::new("tenant.stream"),
..Morphism::default()
};
let err = detect_semantic_cycles(&[first, second, third])
.expect_err("multi-hop boundary cycle should be rejected");
assert!(matches!(
err,
MorphismCompileError::SemanticCycleDetected {
class: SemanticCycleClass::Capability,
ref path,
} if path.len() == 3
));
}
#[test]
fn semantic_cycle_detection_ignores_safe_private_identity_overlap() {
let morphism = Morphism {
source_language: SubjectPattern::new("tenant.local"),
dest_language: SubjectPattern::new("tenant.local"),
..Morphism::default()
};
detect_semantic_cycles(&[morphism]).expect("safe private overlap should be accepted");
}
}