use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SourceId(pub String);
impl SourceId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
}
impl fmt::Display for SourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Trust {
Trusted,
Untrusted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Sensitivity {
Public,
Internal,
Confidential,
Secret,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Label {
pub provenance: BTreeSet<SourceId>,
pub trust: Trust,
pub sensitivity: Sensitivity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProtectedField {
path: String,
#[serde(default)]
require_trusted: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
allowed_sources: BTreeSet<SourceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_sensitivity: Option<Sensitivity>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Release {
scope: ReleaseScope,
basis: String,
destination: String,
fields: BTreeSet<String>,
evidence: BTreeSet<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReleaseScope {
trust: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
sensitivity: Option<Sensitivity>,
}
impl ReleaseScope {
#[must_use]
pub const fn trust() -> Self {
Self {
trust: true,
sensitivity: None,
}
}
#[must_use]
pub const fn sensitivity(target: Sensitivity) -> Self {
Self {
trust: false,
sensitivity: Some(target),
}
}
#[must_use]
pub const fn trust_and_sensitivity(target: Sensitivity) -> Self {
Self {
trust: true,
sensitivity: Some(target),
}
}
}
impl Release {
#[must_use]
pub fn whole(
scope: ReleaseScope,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
Self::new(scope, [String::new()], basis, destination, evidence)
}
#[must_use]
pub fn fields(
scope: ReleaseScope,
fields: impl IntoIterator<Item = String>,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
Self::new(scope, fields, basis, destination, evidence)
}
fn new(
scope: ReleaseScope,
fields: impl IntoIterator<Item = String>,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
let basis = basis.into();
let destination = destination.into();
let fields = fields.into_iter().collect::<BTreeSet<_>>();
let evidence = evidence.into_iter().collect::<BTreeSet<_>>();
assert!(
scope.trust || scope.sensitivity.is_some(),
"a release scope must improve trust, sensitivity, or both"
);
assert!(!basis.trim().is_empty(), "a release must state its basis");
assert!(
!destination.trim().is_empty(),
"a release must name its destination"
);
assert!(!fields.is_empty(), "a release must name at least one field");
assert!(
fields.len() == 1 || !fields.contains(""),
"a whole-value release cannot be mixed with field releases"
);
for path in &fields {
if !path.is_empty() {
assert_json_pointer(path);
}
}
assert!(
!evidence.is_empty() && evidence.iter().all(|item| !item.trim().is_empty()),
"a release must carry non-empty evidence"
);
Self {
scope,
basis,
destination,
fields,
evidence,
}
}
#[must_use]
pub fn basis(&self) -> &str {
&self.basis
}
#[must_use]
pub const fn scope(&self) -> ReleaseScope {
self.scope
}
#[must_use]
pub fn destination(&self) -> &str {
&self.destination
}
#[must_use]
pub fn fields_scope(&self) -> &BTreeSet<String> {
&self.fields
}
#[must_use]
pub fn evidence(&self) -> &BTreeSet<String> {
&self.evidence
}
#[must_use]
pub fn is_whole_value(&self) -> bool {
self.fields.contains("")
}
pub fn validate(&self) -> Result<(), &'static str> {
if !self.scope.trust && self.scope.sensitivity.is_none() {
return Err("scope must improve trust, sensitivity, or both");
}
if self.basis.trim().is_empty() || self.destination.trim().is_empty() {
return Err("basis and destination must be non-empty");
}
if self.fields.is_empty()
|| (self.fields.len() > 1 && self.fields.contains(""))
|| self
.fields
.iter()
.any(|path| !path.is_empty() && !is_json_pointer(path))
{
return Err("field scope is not a valid whole-value or JSON Pointer selection");
}
if self.evidence.is_empty() || self.evidence.iter().any(|item| item.trim().is_empty()) {
return Err("evidence must contain at least one non-empty reference");
}
Ok(())
}
}
impl ProtectedField {
#[must_use]
pub fn trusted(path: impl Into<String>) -> Self {
let path = path.into();
assert_json_pointer(&path);
Self {
path,
require_trusted: true,
allowed_sources: BTreeSet::new(),
max_sensitivity: None,
}
}
#[must_use]
pub fn from_sources(
path: impl Into<String>,
sources: impl IntoIterator<Item = SourceId>,
) -> Self {
let path = path.into();
assert_json_pointer(&path);
let allowed_sources = sources.into_iter().collect::<BTreeSet<_>>();
assert!(
!allowed_sources.is_empty(),
"a protected field source constraint must name at least one source"
);
Self {
path,
require_trusted: false,
allowed_sources,
max_sensitivity: None,
}
}
#[must_use]
pub const fn max_sensitivity(mut self, sensitivity: Sensitivity) -> Self {
self.max_sensitivity = Some(sensitivity);
self
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub const fn requires_trusted(&self) -> bool {
self.require_trusted
}
#[must_use]
pub fn allowed_sources(&self) -> &BTreeSet<SourceId> {
&self.allowed_sources
}
#[must_use]
pub const fn sensitivity_ceiling(&self) -> Option<Sensitivity> {
self.max_sensitivity
}
pub fn validate(&self) -> Result<(), &'static str> {
if !is_json_pointer(&self.path) {
return Err("path must be a non-empty absolute RFC 6901 JSON Pointer");
}
if self.require_trusted && !self.allowed_sources.is_empty() {
return Err("require_trusted and allowed_sources are mutually exclusive");
}
if !self.require_trusted
&& self.allowed_sources.is_empty()
&& self.max_sensitivity.is_none()
{
return Err("at least one trust, source, or sensitivity constraint is required");
}
Ok(())
}
}
fn assert_json_pointer(path: &str) {
assert!(
is_json_pointer(path),
"a protected field path must be a non-empty absolute RFC 6901 JSON Pointer"
);
}
fn is_json_pointer(path: &str) -> bool {
if !path.starts_with('/') {
return false;
}
let bytes = path.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'~' {
index += 1;
if index == bytes.len() || !matches!(bytes[index], b'0' | b'1') {
return false;
}
}
index += 1;
}
true
}
impl Label {
#[must_use]
pub fn trusted() -> Self {
Self {
provenance: BTreeSet::new(),
trust: Trust::Trusted,
sensitivity: Sensitivity::Public,
}
}
#[must_use]
pub fn untrusted(source: SourceId) -> Self {
Self {
provenance: BTreeSet::from([source]),
trust: Trust::Untrusted,
sensitivity: Sensitivity::Internal,
}
}
#[must_use]
pub fn with_sensitivity(mut self, s: Sensitivity) -> Self {
self.sensitivity = s;
self
}
#[must_use]
pub fn join(&self, other: &Self) -> Self {
Self {
provenance: self.provenance.union(&other.provenance).cloned().collect(),
trust: self.trust.max(other.trust),
sensitivity: self.sensitivity.max(other.sensitivity),
}
}
#[must_use]
pub fn is_untrusted(&self) -> bool {
self.trust == Trust::Untrusted
}
}
impl Default for Label {
fn default() -> Self {
Self::trusted()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tainted<T> {
value: T,
label: Label,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
fields: BTreeMap<String, Label>,
}
impl<T> Tainted<T> {
pub fn trusted(value: T) -> Self {
Self {
value,
label: Label::trusted(),
fields: BTreeMap::new(),
}
}
pub fn from_source(value: T, source: SourceId) -> Self {
Self {
value,
label: Label::untrusted(source),
fields: BTreeMap::new(),
}
}
pub fn with_label(value: T, label: Label) -> Self {
Self {
value,
label,
fields: BTreeMap::new(),
}
}
pub fn label(&self) -> &Label {
&self.label
}
#[cfg(feature = "manifest")]
pub(crate) fn with_joined_label(&self, other: &Label) -> Self
where
T: Clone,
{
Self {
value: self.value.clone(),
label: self.label.join(other),
fields: self.fields.clone(),
}
}
pub(crate) fn into_unlabelled(self) -> T {
self.value
}
pub fn peek(&self) -> &T {
&self.value
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Tainted<U> {
Tainted {
value: f(self.value),
label: self.label,
fields: BTreeMap::new(),
}
}
pub fn zip<U>(self, other: Tainted<U>) -> Tainted<(T, U)> {
let label = self.label.join(&other.label);
Tainted {
value: (self.value, other.value),
label,
fields: BTreeMap::new(),
}
}
}
impl Tainted<serde_json::Value> {
pub fn object(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
let mut value = serde_json::Map::new();
let mut label = Label::trusted();
let mut field_labels = BTreeMap::new();
for (name, field) in fields {
let base = format!("/{}", escape_pointer_token(&name));
label = label.join(&field.label);
field_labels.insert(base.clone(), field.label);
for (path, nested) in field.fields {
field_labels.insert(format!("{base}{path}"), nested);
}
value.insert(name, field.value);
}
Self {
value: serde_json::Value::Object(value),
label,
fields: field_labels,
}
}
pub fn array(elements: impl IntoIterator<Item = Self>) -> Self {
let mut value = Vec::new();
let mut label = Label::trusted();
let mut field_labels = BTreeMap::new();
for (index, element) in elements.into_iter().enumerate() {
let base = format!("/{index}");
label = label.join(&element.label);
field_labels.insert(base.clone(), element.label);
for (path, nested) in element.fields {
field_labels.insert(format!("{base}{path}"), nested);
}
value.push(element.value);
}
Self {
value: serde_json::Value::Array(value),
label,
fields: field_labels,
}
}
#[must_use]
pub fn label_at(&self, path: &str) -> Option<&Label> {
self.value.pointer(path)?;
if path.is_empty() {
return Some(&self.label);
}
let mut candidate = path;
loop {
if let Some(label) = self.fields.get(candidate) {
return Some(label);
}
let Some(split) = candidate.rfind('/') else {
return Some(&self.label);
};
if split == 0 {
return Some(&self.label);
}
candidate = &candidate[..split];
}
}
pub fn field_labels(&self) -> impl Iterator<Item = (&str, &Label)> {
self.fields
.iter()
.map(|(path, label)| (path.as_str(), label))
}
pub(crate) fn project_field(&self, name: &str) -> Option<Self> {
let value = self.value.get(name)?.clone();
let base = format!("/{}", escape_pointer_token(name));
let label = self.label_at(&base)?.clone();
let prefix = format!("{base}/");
let fields = self
.fields
.iter()
.filter_map(|(path, label)| {
path.strip_prefix(&prefix)
.map(|relative| (format!("/{relative}"), label.clone()))
})
.collect();
Some(Self {
value,
label,
fields,
})
}
pub(crate) fn apply_release(mut self, release: &Release) -> Option<Self> {
if release.is_whole_value() {
self.label = apply_release_scope(&self.label, release.scope);
for label in self.fields.values_mut() {
*label = apply_release_scope(label, release.scope);
}
return Some(self);
}
if release
.fields
.iter()
.any(|path| !self.fields.contains_key(path) || self.value.pointer(path).is_none())
{
return None;
}
for released in &release.fields {
let descendants = format!("{released}/");
for (path, label) in &mut self.fields {
if path == released || path.starts_with(&descendants) {
*label = apply_release_scope(label, release.scope);
}
}
}
self.label = self
.fields
.values()
.fold(Label::trusted(), |joined, label| joined.join(label));
Some(self)
}
}
fn apply_release_scope(label: &Label, scope: ReleaseScope) -> Label {
let mut released = label.clone();
if scope.trust {
released.trust = Trust::Trusted;
}
if let Some(target) = scope.sensitivity {
released.sensitivity = label.sensitivity.min(target);
}
released
}
fn escape_pointer_token(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
#[cfg(test)]
mod tests {
use super::*;
fn src(s: &str) -> SourceId {
SourceId::new(s)
}
#[test]
fn trust_join_degrades() {
let t = Label::trusted();
let u = Label::untrusted(src("mcp://tool"));
assert_eq!(t.join(&u).trust, Trust::Untrusted);
assert_eq!(u.join(&t).trust, Trust::Untrusted, "join is commutative");
}
#[test]
fn sensitivity_join_escalates() {
let a = Label::trusted().with_sensitivity(Sensitivity::Public);
let b = Label::trusted().with_sensitivity(Sensitivity::Secret);
assert_eq!(a.join(&b).sensitivity, Sensitivity::Secret);
}
#[test]
fn provenance_accumulates() {
let a = Label::untrusted(src("a"));
let b = Label::untrusted(src("b"));
let j = a.join(&b);
assert_eq!(j.provenance.len(), 2);
}
#[test]
fn join_is_idempotent_and_associative() {
let a = Label::untrusted(src("a"));
let b = Label::trusted().with_sensitivity(Sensitivity::Confidential);
let c = Label::untrusted(src("c")).with_sensitivity(Sensitivity::Secret);
assert_eq!(a.join(&a), a, "idempotent");
assert_eq!(a.join(&b).join(&c), a.join(&b.join(&c)), "associative");
}
#[test]
fn zip_propagates_untrust_to_derived_values() {
let trusted = Tainted::trusted(1);
let untrusted = Tainted::from_source(2, src("mcp://tool"));
let combined = trusted.zip(untrusted).map(|(a, b)| a + b);
assert!(combined.label().is_untrusted());
assert_eq!(*combined.peek(), 3);
}
#[test]
fn map_preserves_label() {
let t = Tainted::from_source("x", src("doc"));
let mapped = t.map(str::to_uppercase);
assert!(mapped.label().is_untrusted());
assert_eq!(mapped.peek(), "X");
}
#[test]
fn field_projection_preserves_only_the_selected_lineage() {
let nested = Tainted::object([
("safe".to_owned(), Tainted::trusted(serde_json::json!("ok"))),
(
"body".to_owned(),
Tainted::from_source(serde_json::json!("outside"), src("model")),
),
]);
let value = Tainted::object([
("selected".to_owned(), nested),
(
"other".to_owned(),
Tainted::from_source(serde_json::json!("noise"), src("tool")),
),
]);
let selected = value.project_field("selected").unwrap();
assert!(selected.label_at("/safe").unwrap().provenance.is_empty());
assert_eq!(
selected.label_at("/body").unwrap().provenance,
BTreeSet::from([src("model")])
);
assert!(selected.label_at("/other").is_none());
}
#[test]
fn field_release_improves_only_its_declared_scope() {
let value = Tainted::object([
(
"recipient".to_owned(),
Tainted::from_source(serde_json::json!("treasury"), src("model")),
),
(
"memo".to_owned(),
Tainted::from_source(serde_json::json!("outside"), src("model")),
),
]);
let release = Release::fields(
ReleaseScope::trust(),
["/recipient".to_owned()],
"operator verified account",
"ledger.transfer",
["approval:42".to_owned()],
);
let released = value.apply_release(&release).unwrap();
let recipient = released.label_at("/recipient").unwrap();
assert_eq!(recipient.trust, Trust::Trusted);
assert_eq!(recipient.sensitivity, Sensitivity::Internal);
assert_eq!(recipient.provenance, BTreeSet::from([src("model")]));
assert!(released.label_at("/memo").unwrap().is_untrusted());
assert!(
released.label().is_untrusted(),
"unreleased content still taints"
);
}
}