use serde::{Deserialize, Serialize};
use crate::frame::Representation;
use crate::scope::EgressScope;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataFlow {
#[serde(default)]
pub reads: bool,
#[serde(default)]
pub writes: bool,
#[serde(default)]
pub egress: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub egress_scopes: Vec<EgressScope>,
}
impl DataFlow {
pub fn off_machine_scopes(&self) -> impl Iterator<Item = &EgressScope> {
self.egress_scopes.iter().filter(|s| s.is_off_machine())
}
pub fn scopes_consistent(&self) -> bool {
if !self.egress_scopes.iter().all(EgressScope::is_valid) {
return false;
}
self.egress || self.off_machine_scopes().next().is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderInfo {
pub name: String,
pub version: String,
pub data_flow: DataFlow,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Capabilities {
#[serde(default)]
pub query: QueryCapability,
#[serde(default)]
pub correlation: bool,
#[serde(default)]
pub graph: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embeddings_fingerprint: Option<String>,
#[serde(default)]
pub verify: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub representations: Vec<Representation>,
#[serde(default)]
pub resolve: bool,
}
impl Capabilities {
pub fn offered_representations(&self) -> Vec<Representation> {
if self.representations.is_empty() {
vec![Representation::Full]
} else {
self.representations.clone()
}
}
pub fn representations_consistent(&self) -> bool {
if self.resolve {
return true;
}
!self
.representations
.iter()
.any(|rep| matches!(rep, Representation::Compact | Representation::Reference))
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct QueryCapability {
#[serde(default)]
pub kinds: Vec<String>,
}
pub fn fingerprint_dimensions(fingerprint: &str) -> Option<usize> {
fingerprint.split('/').nth(1)?.parse().ok()
}
pub fn embedding_fingerprints_match(host: &str, provider: &str) -> bool {
!host.is_empty() && host == provider
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scope::EgressScope;
#[test]
fn verify_support_defaults_off() {
let caps = Capabilities::default();
assert!(!caps.verify);
let back: Capabilities =
serde_json::from_str(r#"{"upsert":false,"subscribe":true}"#).unwrap();
assert!(!back.verify);
let pull: Capabilities = serde_json::from_str(r#"{"verify":true}"#).unwrap();
assert!(pull.verify);
}
#[test]
fn egress_provider_data_flow_roundtrips() {
let flow = DataFlow {
reads: true,
writes: false,
egress: true,
egress_scopes: vec![EgressScope::ThirdPartyModel],
};
let json = serde_json::to_string(&flow).unwrap();
let back: DataFlow = serde_json::from_str(&json).unwrap();
assert_eq!(back, flow);
assert!(
back.egress,
"egress providers must be inspectable by hosts before consent"
);
}
#[test]
fn provider_info_defaults_data_flow_to_no_egress() {
let flow = DataFlow::default();
assert!(
!flow.egress,
"default DataFlow must never imply egress consent"
);
assert!(flow.egress_scopes.is_empty());
assert!(flow.scopes_consistent());
}
#[test]
fn empty_egress_scopes_are_omitted_from_the_wire() {
let flow = DataFlow {
reads: true,
writes: false,
egress: false,
egress_scopes: vec![],
};
let json = serde_json::to_string(&flow).unwrap();
assert!(
!json.contains("egress_scopes"),
"an empty scope list must be omitted so the pre-scope wire form is unchanged: {json}"
);
}
#[test]
fn an_off_machine_scope_with_egress_false_is_inconsistent() {
let lying = DataFlow {
reads: true,
writes: false,
egress: false,
egress_scopes: vec![EgressScope::ThirdPartyIndex],
};
assert!(!lying.scopes_consistent());
let honest_local = DataFlow {
reads: true,
writes: false,
egress: false,
egress_scopes: vec![EgressScope::LocalOnly],
};
assert!(honest_local.scopes_consistent());
let honest_egress = DataFlow {
reads: true,
writes: false,
egress: true,
egress_scopes: vec![EgressScope::ThirdPartyModel],
};
assert!(honest_egress.scopes_consistent());
assert_eq!(honest_egress.off_machine_scopes().count(), 1);
let malformed = DataFlow {
reads: true,
writes: false,
egress: true,
egress_scopes: vec![EgressScope::Custom("notnamespaced".into())],
};
assert!(!malformed.scopes_consistent());
}
#[test]
fn capabilities_roundtrip_with_defaults() {
let caps = Capabilities {
query: QueryCapability {
kinds: vec!["snippet".into()],
},
correlation: true,
graph: true,
embeddings_fingerprint: Some("bge-small-en-v1.5/384/l2".into()),
verify: true,
representations: vec![Representation::Full, Representation::Reference],
resolve: true,
};
let json = serde_json::to_string(&caps).unwrap();
let back: Capabilities = serde_json::from_str(&json).unwrap();
assert_eq!(back, caps);
}
#[test]
fn a_provider_still_declaring_the_removed_capabilities_handshakes_successfully() {
let legacy = r#"{
"query": { "kinds": ["doc"], "filters": ["language"] },
"upsert": true,
"graph": false,
"subscribe": true
}"#;
let caps: Capabilities = serde_json::from_str(legacy).expect("legacy ack must still parse");
assert_eq!(caps.query.kinds, vec!["doc".to_string()]);
assert!(!caps.graph);
}
#[test]
fn a_well_formed_fingerprint_yields_its_dimension() {
assert_eq!(
fingerprint_dimensions("bge-small-en-v1.5/384/l2"),
Some(384)
);
assert_eq!(
fingerprint_dimensions("text-embedding-3-large/3072"),
Some(3072)
);
}
#[test]
fn a_fingerprint_without_a_parseable_dimension_yields_none() {
assert_eq!(fingerprint_dimensions("bge-small-v1"), None);
assert_eq!(fingerprint_dimensions("model/not-a-number"), None);
assert_eq!(fingerprint_dimensions(""), None);
}
#[test]
fn fingerprints_match_only_on_exact_equality() {
let provider = "bge-small-en-v1.5/384/l2";
assert!(embedding_fingerprints_match(provider, provider));
assert!(!embedding_fingerprints_match(
"bge-small-en-v1.5/384",
provider
));
assert!(!embedding_fingerprints_match(
"bge-small-en-v1.5/384/none",
provider
));
assert!(!embedding_fingerprints_match(
"text-embedding-3-small/384/l2",
provider
));
}
#[test]
fn an_empty_fingerprint_never_matches_even_itself() {
assert!(!embedding_fingerprints_match("", ""));
}
#[test]
fn representation_capability_defaults_to_full_only_and_is_wire_omitted() {
let caps = Capabilities::default();
assert_eq!(caps.offered_representations(), vec![Representation::Full]);
assert!(caps.representations_consistent());
let json = serde_json::to_string(&caps).unwrap();
assert!(!json.contains("representations"));
let back: Capabilities = serde_json::from_str(r#"{"upsert":false}"#).unwrap();
assert_eq!(back.offered_representations(), vec![Representation::Full]);
assert!(!back.resolve);
}
#[test]
fn reference_or_compact_without_resolve_is_inconsistent() {
let lying = Capabilities {
representations: vec![Representation::Reference],
resolve: false,
..Capabilities::default()
};
assert!(!lying.representations_consistent());
let honest = Capabilities {
representations: vec![Representation::Reference],
resolve: true,
..Capabilities::default()
};
assert!(honest.representations_consistent());
let full_only = Capabilities {
representations: vec![Representation::Full],
resolve: false,
..Capabilities::default()
};
assert!(full_only.representations_consistent());
}
}