use serde::{Deserialize, Serialize};
use crate::llm_config::{
DataControlDef, DataControlDialect, DataControlEffect, DataControlLocation, DataControlScope,
DataControlsDef, DataPosture,
};
use super::dialect::StreamProtocol;
pub(crate) fn dialect_of(protocol: StreamProtocol) -> DataControlDialect {
match protocol {
StreamProtocol::AnthropicSse => DataControlDialect::AnthropicSse,
StreamProtocol::OpenAiSse => DataControlDialect::OpenAiSse,
StreamProtocol::OllamaNdjson => DataControlDialect::OllamaNdjson,
StreamProtocol::GeminiJson => DataControlDialect::GeminiJson,
StreamProtocol::GeminiInteractionsSse => DataControlDialect::GeminiInteractionsSse,
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DataControlsOutcome {
NotRequested,
Applied,
NoControlAvailable,
ProviderUnresearched,
}
impl DataControlsOutcome {
pub fn as_str(self) -> &'static str {
match self {
Self::NotRequested => "not_requested",
Self::Applied => "applied",
Self::NoControlAvailable => "no_control_available",
Self::ProviderUnresearched => "provider_unresearched",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppliedDataControl {
pub location: String,
pub name: String,
pub value: serde_json::Value,
pub effect: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caveat: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DataControlsReceipt {
pub requested_posture: String,
pub outcome: DataControlsOutcome,
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub control_scope: Option<DataControlScope>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub applied: Vec<AppliedDataControl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl DataControlsReceipt {
pub fn as_vm_dict(&self) -> crate::value::VmValue {
let json = serde_json::to_value(self).unwrap_or(serde_json::Value::Null);
crate::schema::json_to_vm_value(&json)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataControlsPlan {
pub body_writes: Vec<(String, serde_json::Value)>,
pub headers: Vec<(String, String)>,
pub receipt: DataControlsReceipt,
}
impl DataControlsPlan {
pub(crate) fn write_body(&self, body: &mut serde_json::Value) {
for (path, value) in &self.body_writes {
set_body_path(body, path, value.clone());
}
}
}
pub(crate) fn resolve(
provider: &str,
dialect: DataControlDialect,
posture: DataPosture,
) -> DataControlsPlan {
let requested_posture = match posture {
DataPosture::Default => "default",
DataPosture::StrictestAvailable => "strictest_available",
}
.to_string();
let declaration = crate::llm_config::provider_config(provider)
.and_then(|definition| definition.data_controls);
if posture == DataPosture::Default {
return DataControlsPlan {
body_writes: Vec::new(),
headers: Vec::new(),
receipt: DataControlsReceipt {
requested_posture,
outcome: DataControlsOutcome::NotRequested,
provider: provider.to_string(),
control_scope: declaration.as_ref().map(|entry| entry.control_scope),
applied: Vec::new(),
note: declaration.and_then(|entry| entry.note),
},
};
}
let Some(declaration) = declaration else {
return DataControlsPlan {
body_writes: Vec::new(),
headers: Vec::new(),
receipt: DataControlsReceipt {
requested_posture,
outcome: DataControlsOutcome::ProviderUnresearched,
provider: provider.to_string(),
control_scope: None,
applied: Vec::new(),
note: None,
},
};
};
let mut body_writes = Vec::new();
let mut headers = Vec::new();
let mut applied = Vec::new();
for control in declaration.controls_for_dialect(dialect) {
match control.location {
DataControlLocation::Body => {
body_writes.push((control.name.clone(), control.value.as_json()));
applied.push(applied_receipt(control, control.value.as_json()));
}
DataControlLocation::Header => {
let rendered = control.value.as_header_value();
headers.push((control.name.clone(), rendered.clone()));
applied.push(applied_receipt(
control,
serde_json::Value::String(rendered),
));
}
}
}
let outcome = if applied.is_empty() {
DataControlsOutcome::NoControlAvailable
} else {
DataControlsOutcome::Applied
};
DataControlsPlan {
body_writes,
headers,
receipt: DataControlsReceipt {
requested_posture,
outcome,
provider: provider.to_string(),
control_scope: Some(declaration.control_scope),
applied,
note: declaration.note,
},
}
}
fn applied_receipt(control: &DataControlDef, value: serde_json::Value) -> AppliedDataControl {
AppliedDataControl {
location: match control.location {
DataControlLocation::Body => "body",
DataControlLocation::Header => "header",
}
.to_string(),
name: control.name.clone(),
value,
effect: match control.effect {
DataControlEffect::Retention => "retention",
DataControlEffect::Training => "training",
}
.to_string(),
caveat: control.caveat.clone(),
}
}
fn set_body_path(body: &mut serde_json::Value, path: &str, value: serde_json::Value) {
let mut cursor = body;
let mut segments = path.split('.').peekable();
while let Some(segment) = segments.next() {
if segments.peek().is_none() {
if let Some(map) = cursor.as_object_mut() {
map.insert(segment.to_string(), value);
}
return;
}
if !cursor.get(segment).is_some_and(|child| child.is_object()) {
if let Some(map) = cursor.as_object_mut() {
map.insert(segment.to_string(), serde_json::json!({}));
} else {
return;
}
}
cursor = match cursor.get_mut(segment) {
Some(child) => child,
None => return,
};
}
}
pub fn declaration_for(provider: &str) -> Option<DataControlsDef> {
crate::llm_config::provider_config(provider).and_then(|entry| entry.data_controls)
}
#[cfg(test)]
#[path = "data_controls_tests.rs"]
mod data_controls_tests;