//! Dependency-graph proof of the privacy boundary.
//!
//! The rendezvous registry's claim is that nothing but similarity digests ever
//! leaves an organisation. Half of that claim is a trait bound
//! (`jac_core::DigestSafe`); this is the other half, and the stronger one: a
//! rendezvous-plane crate cannot *name* a content type, because the dependency
//! graph never lets it reach one.
//!
//! Each workspace member declares its plane in `Cargo.toml`:
//!
//! ```toml
//! [package.metadata.jacquard]
//! plane = "content" # neutral | content | rendezvous
//! forbids = ["jac-rendezvous"] # additional crates this one may never reach
//! ```
//!
//! Two rules are enforced:
//!
//! 1. **Privacy.** A `rendezvous` crate may not transitively reach a `content` crate. A
//! `neutral` crate may not reach `content` or `rendezvous`: neutral crates are the
//! shared vocabulary, safe to link anywhere, and a neutral crate that reaches upward
//! is not neutral any more.
//! 2. **Blinding.** A crate may not transitively reach anything in its `forbids` list.
//! This is how "the registry cannot see the cloth" becomes a fact the build checks
//! rather than a convention.
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use anyhow::{Context as _, Result, bail};
use cargo_metadata::{DependencyKind, MetadataCommand, Node, Package, PackageId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Plane {
Neutral,
Content,
Rendezvous,
}
impl Plane {
fn parse(s: &str) -> Result<Self> {
Ok(match s {
"neutral" => Self::Neutral,
"content" => Self::Content,
"rendezvous" => Self::Rendezvous,
other => bail!("unknown plane `{other}`"),
})
}
}
#[derive(Debug)]
struct Declared {
plane: Plane,
forbids: Vec<String>,
}
pub(crate) fn run() -> Result<()> {
let metadata = MetadataCommand::new()
.manifest_path(crate::workspace_root()?.join("Cargo.toml"))
.exec()
.context("failed to run `cargo metadata`")?;
let by_id: BTreeMap<&PackageId, &Package> =
metadata.packages.iter().map(|p| (&p.id, p)).collect();
let resolve = metadata.resolve.as_ref().context(
"`cargo metadata` returned no resolve graph; the boundary proof cannot be made \
without one and must not be skipped",
)?;
let nodes: BTreeMap<&PackageId, &Node> = resolve.nodes.iter().map(|n| (&n.id, n)).collect();
// Declared plane per workspace member. Crates from the registry carry no
// declaration and are treated as neutral: they cannot name our types.
let mut declared: BTreeMap<&PackageId, Declared> = BTreeMap::new();
for id in &metadata.workspace_members {
let pkg = by_id
.get(id)
.with_context(|| format!("workspace member {id} not in packages"))?;
declared.insert(id, read_declaration(pkg)?);
}
let mut violations = Vec::new();
for (id, decl) in &declared {
let pkg = by_id.get(*id).context("member vanished from package map")?;
let reachable = reachable_from(id, &nodes);
for dep in &reachable {
let Some(dep_decl) = declared.get(dep) else {
continue;
};
let dep_name = by_id.get(dep).map_or("<unknown>", |p| p.name.as_str());
let broken = match decl.plane {
// The privacy rule: digests-only crates never reach content.
Plane::Rendezvous => dep_decl.plane == Plane::Content,
// Shared vocabulary reaches nothing above itself.
Plane::Neutral => dep_decl.plane != Plane::Neutral,
Plane::Content => false,
};
if broken {
violations.push(format!(
"privacy: {:?}-plane crate `{}` transitively links {:?}-plane crate `{}`",
decl.plane, pkg.name, dep_decl.plane, dep_name
));
}
}
for forbidden in &decl.forbids {
let hit = reachable
.iter()
.filter_map(|d| by_id.get(d))
.any(|p| p.name.as_str() == forbidden.as_str());
if hit {
violations.push(format!(
"blinding: crate `{}` declares it may never reach `{}`, but the dependency \
graph says it does",
pkg.name, forbidden
));
}
}
}
if !violations.is_empty() {
for v in &violations {
eprintln!(" {v}");
}
bail!(
"{} boundary violation(s); the privacy argument does not hold",
violations.len()
);
}
let rendezvous: Vec<&str> = declared
.iter()
.filter(|(_, d)| d.plane == Plane::Rendezvous)
.filter_map(|(id, _)| by_id.get(id).map(|p| p.name.as_str()))
.collect();
eprintln!(
"boundary: ok ({} member(s) checked, digest-only: {})",
declared.len(),
if rendezvous.is_empty() {
"<none yet>".to_owned()
} else {
rendezvous.join(", ")
}
);
Ok(())
}
fn read_declaration(pkg: &Package) -> Result<Declared> {
let node = pkg.metadata.get("jacquard").with_context(|| {
format!(
"crate `{}` does not declare `[package.metadata.jacquard] plane`. Every workspace \
member must declare its plane; an undeclared crate cannot be proven anything.",
pkg.name
)
})?;
let plane = node
.get("plane")
.and_then(|v| v.as_str())
.with_context(|| format!("crate `{}` has no `plane` key", pkg.name))?;
let forbids = node
.get("forbids")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
Ok(Declared {
plane: Plane::parse(plane)?,
forbids,
})
}
/// Transitive closure over normal and build dependencies.
///
/// Dev-dependencies are excluded deliberately: a test may legitimately link a
/// content crate to assert a property about the boundary, and that edge never
/// reaches a shipped artifact.
fn reachable_from<'a>(
start: &'a PackageId,
nodes: &BTreeMap<&'a PackageId, &'a Node>,
) -> BTreeSet<&'a PackageId> {
let mut seen = BTreeSet::new();
let mut queue = VecDeque::from([start]);
while let Some(id) = queue.pop_front() {
let Some(node) = nodes.get(id) else { continue };
for dep in &node.deps {
let is_linked = dep
.dep_kinds
.iter()
.any(|k| matches!(k.kind, DependencyKind::Normal | DependencyKind::Build));
if !is_linked {
continue;
}
let Some((next, _)) = nodes.get_key_value(&dep.pkg) else {
continue;
};
if seen.insert(*next) {
queue.push_back(*next);
}
}
}
seen
}