use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Capability {
Network {
#[serde(default)]
allow: Vec<SmolStr>,
},
Filesystem {
#[serde(default)]
read: Vec<SmolStr>,
#[serde(default)]
write: Vec<SmolStr>,
},
HostQuery {
#[serde(default)]
read_only: bool,
#[serde(default)]
scopes: Vec<SmolStr>,
},
Kms {
#[serde(default)]
key_ids: Vec<SmolStr>,
},
Secret {
#[serde(default)]
ids: Vec<SmolStr>,
},
Lock {
granularity: LockGranularity,
},
Config {
#[serde(default)]
keys: Vec<SmolStr>,
},
PluginStorage,
ScalarFn,
AggregateFn,
WindowFn,
Procedure,
ProcedureWrites,
ProcedureSchema,
ProcedureDbms,
LocyAggregate,
LocyPredicate,
LocyGenerator,
Operator,
Index,
Storage,
Algorithm,
GraphCompute,
Crdt,
Hook,
Trigger,
BackgroundJob {
max_concurrent: u32,
},
Type,
Auth,
Authz,
Collation,
Cdc,
Catalog,
PluginDeclare,
MemoryBytes(u64),
FuelPerCall(u64),
WallClockMillisPerCall(u64),
ConcurrentInstances(u32),
TotalMemoryBytes(u64),
MaxResultRows(u64),
GraphComputeWork(u64),
GraphComputeArenaBytes(u64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum LockGranularity {
Nodes,
Edges,
Both,
Global,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CapabilitySet {
set: BTreeSet<Capability>,
}
impl CapabilitySet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_iter_of(caps: impl IntoIterator<Item = Capability>) -> Self {
Self {
set: caps.into_iter().collect(),
}
}
#[must_use]
pub fn from_manifest(caps: impl IntoIterator<Item = ManifestCapability>) -> Self {
Self::from_iter_of(caps.into_iter().map(|m| m.0))
}
pub fn insert(&mut self, cap: Capability) -> bool {
self.set.insert(cap)
}
#[must_use]
pub fn contains(&self, cap: &Capability) -> bool {
self.set.contains(cap)
}
#[must_use]
pub fn contains_variant(&self, target: &Capability) -> bool {
self.set.iter().any(|c| variant_matches(c, target))
}
#[must_use]
pub fn intersect(&self, other: &Self) -> Self {
let mut out = Self::new();
for c in &self.set {
if other.contains_variant(c) {
out.insert(attenuate_to_host(c, other));
}
}
out
}
#[must_use]
pub fn denied_against(&self, effective: &CapabilitySet) -> Vec<Capability> {
self.set
.iter()
.filter(|c| !effective.contains_variant(c))
.cloned()
.collect()
}
pub fn iter(&self) -> impl Iterator<Item = &Capability> {
self.set.iter()
}
#[must_use]
pub fn len(&self) -> usize {
self.set.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.set.is_empty()
}
}
fn variant_matches(a: &Capability, b: &Capability) -> bool {
std::mem::discriminant(a) == std::mem::discriminant(b)
}
fn attenuate_to_host(guest: &Capability, host: &CapabilitySet) -> Capability {
match guest {
Capability::Network { allow } => Capability::Network {
allow: intersect_globs(allow, &host_lists(host, network_allow)),
},
Capability::Filesystem { read, write } => Capability::Filesystem {
read: intersect_globs(read, &host_lists(host, fs_read)),
write: intersect_globs(write, &host_lists(host, fs_write)),
},
Capability::Kms { key_ids } => Capability::Kms {
key_ids: intersect_globs(key_ids, &host_lists(host, kms_ids)),
},
Capability::Secret { ids } => Capability::Secret {
ids: intersect_globs(ids, &host_lists(host, secret_ids)),
},
Capability::Config { keys } => Capability::Config {
keys: intersect_globs(keys, &host_lists(host, config_keys)),
},
Capability::HostQuery { read_only, scopes } => {
let host_read_only = host.set.iter().any(|c| {
matches!(
c,
Capability::HostQuery {
read_only: true,
..
}
)
});
let host_scopes = host_lists(host, host_query_scopes);
let scopes = if scopes.is_empty() {
host_scopes
} else if host_scopes.is_empty() {
scopes.clone()
} else {
intersect_globs(scopes, &host_scopes)
};
Capability::HostQuery {
read_only: *read_only || host_read_only,
scopes,
}
}
other => other.clone(),
}
}
fn network_allow(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Network { allow } => Some(allow),
_ => None,
}
}
fn fs_read(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Filesystem { read, .. } => Some(read),
_ => None,
}
}
fn fs_write(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Filesystem { write, .. } => Some(write),
_ => None,
}
}
fn kms_ids(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Kms { key_ids } => Some(key_ids),
_ => None,
}
}
fn secret_ids(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Secret { ids } => Some(ids),
_ => None,
}
}
fn config_keys(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::Config { keys } => Some(keys),
_ => None,
}
}
fn host_query_scopes(c: &Capability) -> Option<&[SmolStr]> {
match c {
Capability::HostQuery { scopes, .. } => Some(scopes),
_ => None,
}
}
fn host_lists<'a>(
host: &'a CapabilitySet,
extract: impl Fn(&'a Capability) -> Option<&'a [SmolStr]>,
) -> Vec<SmolStr> {
host.set
.iter()
.filter_map(extract)
.flatten()
.cloned()
.collect()
}
fn intersect_globs(a: &[SmolStr], b: &[SmolStr]) -> Vec<SmolStr> {
let mut out: Vec<SmolStr> = Vec::new();
let mut keep = |pat: &SmolStr, ceiling: &[SmolStr]| {
if ceiling.iter().any(|q| wildcard_match(q, pat)) && !out.contains(pat) {
out.push(pat.clone());
}
};
for pat in a {
keep(pat, b);
}
for pat in b {
keep(pat, a);
}
out
}
impl Capability {
#[must_use]
pub fn network_allows(&self, url: &str) -> bool {
matches!(self, Capability::Network { allow } if allow.iter().any(|p| wildcard_match(p, url)))
}
#[must_use]
pub fn kms_allows(&self, key_id: &str) -> bool {
matches!(self, Capability::Kms { key_ids } if key_ids.iter().any(|p| wildcard_match(p, key_id)))
}
#[must_use]
pub fn secret_allows(&self, id: &str) -> bool {
matches!(self, Capability::Secret { ids } if ids.iter().any(|p| wildcard_match(p, id)))
}
#[must_use]
pub fn filesystem_read_allows(&self, path: &str) -> bool {
matches!(self, Capability::Filesystem { read, .. } if read.iter().any(|p| wildcard_match(p, path)))
}
#[must_use]
pub fn filesystem_write_allows(&self, path: &str) -> bool {
matches!(self, Capability::Filesystem { write, .. } if write.iter().any(|p| wildcard_match(p, path)))
}
}
const GRANTABLE_NAMES: &[&str] = &[
"ScalarFn",
"AggregateFn",
"WindowFn",
"Procedure",
"ProcedureWrites",
"ProcedureSchema",
"ProcedureDbms",
"LocyAggregate",
"LocyPredicate",
"LocyGenerator",
"Operator",
"Index",
"Storage",
"Algorithm",
"GraphCompute",
"Crdt",
"Hook",
"Trigger",
"Type",
"Collation",
"PluginStorage",
"Network",
"Filesystem",
"HostQuery",
"Kms",
"Secret",
"Config",
"Lock",
];
const QUOTA_NAMES: &[&str] = &[
"BackgroundJob",
"MemoryBytes",
"FuelPerCall",
"WallClockMillisPerCall",
"ConcurrentInstances",
"TotalMemoryBytes",
"MaxResultRows",
"GraphComputeWork",
"GraphComputeArenaBytes",
];
const INTERNAL_NAMES: &[&str] = &["Auth", "Authz", "Cdc", "Catalog", "PluginDeclare"];
fn grant_key(s: &str) -> String {
s.chars()
.filter(|c| *c != '-')
.map(|c| c.to_ascii_lowercase())
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum GrantError {
#[error("unknown grant `{name}`; grantable capabilities: {supported}")]
Unknown {
name: String,
supported: String,
},
#[error(
"grant `{name}` is a resource quota; declare it with a value in the \
plugin manifest `capabilities:` list, not as a bare grant"
)]
Quota {
name: String,
},
#[error("grant `{name}` is not grantable to guest plugins")]
Internal {
name: String,
},
}
impl Capability {
#[must_use]
pub fn grant_name(&self) -> &'static str {
match self {
Capability::Network { .. } => "Network",
Capability::Filesystem { .. } => "Filesystem",
Capability::HostQuery { .. } => "HostQuery",
Capability::Kms { .. } => "Kms",
Capability::Secret { .. } => "Secret",
Capability::Lock { .. } => "Lock",
Capability::Config { .. } => "Config",
Capability::PluginStorage => "PluginStorage",
Capability::ScalarFn => "ScalarFn",
Capability::AggregateFn => "AggregateFn",
Capability::WindowFn => "WindowFn",
Capability::Procedure => "Procedure",
Capability::ProcedureWrites => "ProcedureWrites",
Capability::ProcedureSchema => "ProcedureSchema",
Capability::ProcedureDbms => "ProcedureDbms",
Capability::LocyAggregate => "LocyAggregate",
Capability::LocyPredicate => "LocyPredicate",
Capability::LocyGenerator => "LocyGenerator",
Capability::Operator => "Operator",
Capability::Index => "Index",
Capability::Storage => "Storage",
Capability::Algorithm => "Algorithm",
Capability::GraphCompute => "GraphCompute",
Capability::Crdt => "Crdt",
Capability::Hook => "Hook",
Capability::Trigger => "Trigger",
Capability::BackgroundJob { .. } => "BackgroundJob",
Capability::Type => "Type",
Capability::Auth => "Auth",
Capability::Authz => "Authz",
Capability::Collation => "Collation",
Capability::Cdc => "Cdc",
Capability::Catalog => "Catalog",
Capability::PluginDeclare => "PluginDeclare",
Capability::MemoryBytes(_) => "MemoryBytes",
Capability::FuelPerCall(_) => "FuelPerCall",
Capability::WallClockMillisPerCall(_) => "WallClockMillisPerCall",
Capability::ConcurrentInstances(_) => "ConcurrentInstances",
Capability::TotalMemoryBytes(_) => "TotalMemoryBytes",
Capability::MaxResultRows(_) => "MaxResultRows",
Capability::GraphComputeWork(_) => "GraphComputeWork",
Capability::GraphComputeArenaBytes(_) => "GraphComputeArenaBytes",
}
}
#[must_use]
pub fn grantable_names() -> &'static [&'static str] {
GRANTABLE_NAMES
}
pub fn parse_grant(s: &str) -> Result<Self, GrantError> {
let key = grant_key(s);
if let Some(cap) = grant_default_for_key(&key) {
return Ok(cap);
}
if let Some(name) = QUOTA_NAMES.iter().find(|n| grant_key(n) == key) {
return Err(GrantError::Quota {
name: (*name).to_owned(),
});
}
if let Some(name) = INTERNAL_NAMES.iter().find(|n| grant_key(n) == key) {
return Err(GrantError::Internal {
name: (*name).to_owned(),
});
}
Err(GrantError::Unknown {
name: s.to_owned(),
supported: GRANTABLE_NAMES.join(" / "),
})
}
}
fn grant_default_for_key(key: &str) -> Option<Capability> {
Some(match key {
"scalarfn" => Capability::ScalarFn,
"aggregatefn" => Capability::AggregateFn,
"windowfn" => Capability::WindowFn,
"procedure" => Capability::Procedure,
"procedurewrites" => Capability::ProcedureWrites,
"procedureschema" => Capability::ProcedureSchema,
"proceduredbms" => Capability::ProcedureDbms,
"locyaggregate" => Capability::LocyAggregate,
"locypredicate" => Capability::LocyPredicate,
"locygenerator" => Capability::LocyGenerator,
"operator" => Capability::Operator,
"index" => Capability::Index,
"storage" => Capability::Storage,
"algorithm" => Capability::Algorithm,
"graphcompute" => Capability::GraphCompute,
"crdt" => Capability::Crdt,
"hook" => Capability::Hook,
"trigger" => Capability::Trigger,
"type" => Capability::Type,
"collation" => Capability::Collation,
"pluginstorage" => Capability::PluginStorage,
"network" => Capability::Network {
allow: vec!["**".into()],
},
"filesystem" => Capability::Filesystem {
read: vec!["**".into()],
write: vec!["**".into()],
},
"hostquery" => Capability::HostQuery {
read_only: true,
scopes: vec!["**".into()],
},
"kms" => Capability::Kms {
key_ids: vec!["*".into()],
},
"secret" => Capability::Secret {
ids: vec!["*".into()],
},
"config" => Capability::Config {
keys: vec!["**".into()],
},
"lock" => Capability::Lock {
granularity: LockGranularity::Global,
},
_ => return None,
})
}
#[derive(Clone, Debug)]
pub struct ManifestCapability(pub Capability);
impl<'de> Deserialize<'de> for ManifestCapability {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Bare(String),
Full(Capability),
}
let cap = match Repr::deserialize(deserializer)? {
Repr::Full(c) => c,
Repr::Bare(name) => {
let tagged = serde_json::json!({ "kind": name });
Capability::deserialize(tagged).map_err(serde::de::Error::custom)?
}
};
Ok(ManifestCapability(cap))
}
}
fn wildcard_match(pattern: &str, text: &str) -> bool {
let p = pattern.as_bytes();
let t = text.as_bytes();
let (mut pi, mut ti) = (0usize, 0usize);
let mut star: Option<usize> = None;
let mut mark = 0usize;
while ti < t.len() {
if pi < p.len() && p[pi] == b'*' {
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
if pi == p.len() {
return true;
}
star = Some(pi);
mark = ti;
} else if pi < p.len() && p[pi] == t[ti] {
pi += 1;
ti += 1;
} else if let Some(s) = star {
pi = s;
mark += 1;
ti = mark;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Determinism {
Pure,
SessionScoped,
#[default]
Nondeterministic,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SideEffects {
#[default]
ReadOnly,
Writes,
ExternalIo,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Scope {
#[default]
Instance,
Session,
}
#[cfg(test)]
mod tests {
use super::*;
fn all_capability_variants() -> Vec<Capability> {
vec![
Capability::Network { allow: vec![] },
Capability::Filesystem {
read: vec![],
write: vec![],
},
Capability::HostQuery {
read_only: true,
scopes: vec![],
},
Capability::Kms { key_ids: vec![] },
Capability::Secret { ids: vec![] },
Capability::Lock {
granularity: LockGranularity::Both,
},
Capability::Config { keys: vec![] },
Capability::PluginStorage,
Capability::ScalarFn,
Capability::AggregateFn,
Capability::WindowFn,
Capability::Procedure,
Capability::ProcedureWrites,
Capability::ProcedureSchema,
Capability::ProcedureDbms,
Capability::LocyAggregate,
Capability::LocyPredicate,
Capability::LocyGenerator,
Capability::Operator,
Capability::Index,
Capability::Storage,
Capability::Algorithm,
Capability::GraphCompute,
Capability::Crdt,
Capability::Hook,
Capability::Trigger,
Capability::BackgroundJob { max_concurrent: 1 },
Capability::Type,
Capability::Auth,
Capability::Authz,
Capability::Collation,
Capability::Cdc,
Capability::Catalog,
Capability::PluginDeclare,
Capability::MemoryBytes(0),
Capability::FuelPerCall(0),
Capability::WallClockMillisPerCall(0),
Capability::ConcurrentInstances(0),
Capability::TotalMemoryBytes(0),
Capability::MaxResultRows(0),
Capability::GraphComputeWork(0),
Capability::GraphComputeArenaBytes(0),
]
}
#[test]
fn every_variant_classified_exactly_once() {
let variants = all_capability_variants();
assert_eq!(
variants.len(),
GRANTABLE_NAMES.len() + QUOTA_NAMES.len() + INTERNAL_NAMES.len(),
"every variant must be represented and classified exactly once",
);
for cap in variants {
let name = cap.grant_name();
let grantable = GRANTABLE_NAMES.contains(&name);
let quota = QUOTA_NAMES.contains(&name);
let internal = INTERNAL_NAMES.contains(&name);
assert!(
[grantable, quota, internal].iter().filter(|b| **b).count() == 1,
"`{name}` must fall in exactly one grant class",
);
}
}
#[test]
fn grantable_names_round_trip() {
for name in GRANTABLE_NAMES {
let cap = Capability::parse_grant(name)
.unwrap_or_else(|e| panic!("`{name}` should be grantable: {e}"));
assert_eq!(cap.grant_name(), *name);
}
}
#[test]
fn parse_grant_accepts_pascal_and_kebab() {
assert_eq!(
Capability::parse_grant("GraphCompute").unwrap(),
Capability::GraphCompute,
);
assert_eq!(
Capability::parse_grant("graph-compute").unwrap(),
Capability::GraphCompute,
);
assert_eq!(
Capability::parse_grant("Algorithm").unwrap(),
Capability::Algorithm,
);
assert!(matches!(
Capability::parse_grant("HostQuery").unwrap(),
Capability::HostQuery { read_only: true, scopes } if scopes == vec![SmolStr::new("**")]
));
}
#[test]
fn parse_grant_rejects_quota_internal_unknown() {
assert!(matches!(
Capability::parse_grant("MemoryBytes"),
Err(GrantError::Quota { .. })
));
assert!(matches!(
Capability::parse_grant("BackgroundJob"),
Err(GrantError::Quota { .. })
));
assert!(matches!(
Capability::parse_grant("Auth"),
Err(GrantError::Internal { .. })
));
assert!(matches!(
Capability::parse_grant("PluginDeclare"),
Err(GrantError::Internal { .. })
));
assert!(matches!(
Capability::parse_grant("NotARealCapability"),
Err(GrantError::Unknown { .. })
));
}
#[test]
fn denied_against_ignores_attenuated_but_granted_payload() {
let declared = CapabilitySet::from_iter_of([
Capability::HostQuery {
read_only: true,
scopes: vec![SmolStr::new("a")],
},
Capability::Algorithm,
]);
let granted = CapabilitySet::from_iter_of([
Capability::HostQuery {
read_only: true,
scopes: vec![SmolStr::new("a"), SmolStr::new("b")],
},
]);
let effective = declared.intersect(&granted);
let denied = declared.denied_against(&effective);
assert_eq!(denied, vec![Capability::Algorithm]);
}
#[test]
fn capability_set_default_empty() {
let s = CapabilitySet::new();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
}
#[test]
fn capability_set_insert_dedup() {
let mut s = CapabilitySet::new();
assert!(s.insert(Capability::ScalarFn));
assert!(!s.insert(Capability::ScalarFn));
assert_eq!(s.len(), 1);
}
#[test]
fn intersect_keeps_matching_variants() {
let a = CapabilitySet::from_iter_of([
Capability::ScalarFn,
Capability::Storage,
Capability::Network {
allow: vec![SmolStr::new("https://api.example/**")],
},
]);
let b = CapabilitySet::from_iter_of([
Capability::ScalarFn,
Capability::Network {
allow: vec![SmolStr::new("https://api.example/**")],
},
]);
let inter = a.intersect(&b);
assert!(inter.contains(&Capability::ScalarFn));
assert!(!inter.contains_variant(&Capability::Storage));
assert!(inter.contains_variant(&Capability::Network { allow: vec![] }));
}
#[test]
fn graph_compute_work_grant_survives_attenuation_verbatim() {
let big = 5_000_000_000u64; let guest = CapabilitySet::from_iter_of([
Capability::GraphCompute,
Capability::GraphComputeWork(big),
]);
let host = CapabilitySet::from_iter_of([
Capability::GraphCompute,
Capability::GraphComputeWork(big),
]);
let inter = guest.intersect(&host);
let work = inter.iter().find_map(|c| match c {
Capability::GraphComputeWork(w) => Some(*w),
_ => None,
});
assert_eq!(
work,
Some(big),
"the work grant must survive attenuation unchanged"
);
}
#[test]
fn work_grant_is_independent_of_arena_and_wallclock() {
let caps = CapabilitySet::from_iter_of([
Capability::GraphComputeWork(1_234),
Capability::GraphComputeArenaBytes(9_999),
Capability::WallClockMillisPerCall(42),
]);
let inter = caps.intersect(&caps);
let mut work = None;
let mut arena = None;
let mut wall = None;
for c in inter.iter() {
match c {
Capability::GraphComputeWork(w) => work = Some(*w),
Capability::GraphComputeArenaBytes(b) => arena = Some(*b),
Capability::WallClockMillisPerCall(ms) => wall = Some(*ms),
_ => {}
}
}
assert_eq!(work, Some(1_234));
assert_eq!(
arena,
Some(9_999),
"arena cap must be untouched by the work grant"
);
assert_eq!(
wall,
Some(42),
"wall-clock must be untouched by the work grant"
);
}
#[test]
fn intersect_attenuates_network_to_host_ceiling() {
let guest = CapabilitySet::from_iter_of([Capability::Network {
allow: vec![SmolStr::new("**")],
}]);
let host = CapabilitySet::from_iter_of([Capability::Network {
allow: vec![SmolStr::new("https://api.example/**")],
}]);
let effective = guest.intersect(&host);
assert!(
effective
.iter()
.any(|c| c.network_allows("https://api.example/v1/x")),
"host-permitted URL must remain allowed"
);
assert!(
!effective
.iter()
.any(|c| c.network_allows("https://evil.example/x")),
"guest's `**` must not survive the host ceiling — sandbox escape"
);
}
#[test]
fn intersect_keeps_guest_when_narrower_than_host() {
let guest = CapabilitySet::from_iter_of([Capability::Network {
allow: vec![SmolStr::new("https://api.example/v1/**")],
}]);
let host = CapabilitySet::from_iter_of([Capability::Network {
allow: vec![SmolStr::new("https://api.example/**")],
}]);
let effective = guest.intersect(&host);
assert!(
effective
.iter()
.any(|c| c.network_allows("https://api.example/v1/x"))
);
assert!(
!effective
.iter()
.any(|c| c.network_allows("https://api.example/v2/x")),
"guest's own restriction must still bind"
);
}
#[test]
fn intersect_attenuates_kms_secret_fs() {
let guest = CapabilitySet::from_iter_of([
Capability::Kms {
key_ids: vec![SmolStr::new("**")],
},
Capability::Secret {
ids: vec![SmolStr::new("**")],
},
Capability::Filesystem {
read: vec![SmolStr::new("**")],
write: vec![SmolStr::new("**")],
},
]);
let host = CapabilitySet::from_iter_of([
Capability::Kms {
key_ids: vec![SmolStr::new("prod/signing/**")],
},
Capability::Secret {
ids: vec![SmolStr::new("db/**")],
},
Capability::Filesystem {
read: vec![SmolStr::new("/data/**")],
write: vec![], },
]);
let effective = guest.intersect(&host);
assert!(effective.iter().any(|c| c.kms_allows("prod/signing/key1")));
assert!(!effective.iter().any(|c| c.kms_allows("dev/key")));
assert!(effective.iter().any(|c| c.secret_allows("db/password")));
assert!(!effective.iter().any(|c| c.secret_allows("kms/root")));
assert!(
!effective.iter().any(|c| matches!(
c,
Capability::Filesystem { write, .. } if !write.is_empty()
)),
"guest write `**` must not survive an empty host write grant"
);
}
#[test]
fn contains_variant_ignores_attenuation() {
let s = CapabilitySet::from_iter_of([Capability::Network {
allow: vec![SmolStr::new("https://x.example/*")],
}]);
assert!(s.contains_variant(&Capability::Network { allow: vec![] }));
assert!(!s.contains(&Capability::Network { allow: vec![] }));
}
#[test]
fn determinism_default_is_nondeterministic() {
assert_eq!(Determinism::default(), Determinism::Nondeterministic);
}
#[test]
fn wildcard_match_basics() {
assert!(wildcard_match("*", "anything"));
assert!(wildcard_match("**", "any/thing"));
assert!(wildcard_match(
"https://api.example/**",
"https://api.example/v1/x"
));
assert!(wildcard_match("exact", "exact"));
assert!(!wildcard_match("exact", "other"));
assert!(!wildcard_match(
"https://api.example/**",
"https://evil.example/x"
));
assert!(wildcard_match("a*c", "abbbc"));
assert!(!wildcard_match("a*c", "abbb"));
}
#[test]
fn network_allows_matches_only_network_variant() {
let net = Capability::Network {
allow: vec![SmolStr::new("https://api.example/**")],
};
assert!(net.network_allows("https://api.example/v1/data"));
assert!(!net.network_allows("https://evil.example/x"));
assert!(!Capability::ScalarFn.network_allows("https://api.example/x"));
}
#[test]
fn kms_and_secret_allow_wildcard_and_exact() {
let kms = Capability::Kms {
key_ids: vec![SmolStr::new("*")],
};
assert!(kms.kms_allows("signing-key-1"));
let secret = Capability::Secret {
ids: vec![SmolStr::new("db-password")],
};
assert!(secret.secret_allows("db-password"));
assert!(!secret.secret_allows("other"));
}
#[test]
fn manifest_capability_parses_bare_and_structured() {
let bare: ManifestCapability = serde_json::from_str("\"network\"").unwrap();
assert!(matches!(&bare.0, Capability::Network { allow } if allow.is_empty()));
assert!(!bare.0.network_allows("https://api.example/x"));
let scalar: ManifestCapability = serde_json::from_str("\"scalar-fn\"").unwrap();
assert_eq!(scalar.0, Capability::ScalarFn);
let structured: ManifestCapability =
serde_json::from_str(r#"{"kind":"network","allow":["https://api.example/**"]}"#)
.unwrap();
assert!(structured.0.network_allows("https://api.example/v1/x"));
assert!(!structured.0.network_allows("https://evil.example/x"));
let set = CapabilitySet::from_manifest([bare, scalar, structured]);
assert!(set.contains_variant(&Capability::Network { allow: vec![] }));
assert!(set.contains(&Capability::ScalarFn));
}
#[test]
fn filesystem_allows_read_and_write_separately() {
let fs = Capability::Filesystem {
read: vec![SmolStr::new("/data/**")],
write: vec![SmolStr::new("/tmp/out/**")],
};
assert!(fs.filesystem_read_allows("/data/x/y.txt"));
assert!(!fs.filesystem_read_allows("/etc/passwd"));
assert!(fs.filesystem_write_allows("/tmp/out/log"));
assert!(!fs.filesystem_write_allows("/data/x/y.txt"));
assert!(!Capability::ScalarFn.filesystem_read_allows("/data/x"));
}
}