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,
Operator,
Index,
Storage,
Algorithm,
Crdt,
Hook,
Trigger,
BackgroundJob {
max_concurrent: u32,
},
Type,
Auth,
Authz,
Connector,
Collation,
Cdc,
Catalog,
PluginDeclare,
MemoryBytes(u64),
FuelPerCall(u64),
WallClockMillisPerCall(u64),
ConcurrentInstances(u32),
TotalMemoryBytes(u64),
MaxResultRows(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
}
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, |c| network_allow(c))),
},
Capability::Filesystem { read, write } => Capability::Filesystem {
read: intersect_globs(read, &host_lists(host, |c| fs_read(c))),
write: intersect_globs(write, &host_lists(host, |c| fs_write(c))),
},
Capability::Kms { key_ids } => Capability::Kms {
key_ids: intersect_globs(key_ids, &host_lists(host, |c| kms_ids(c))),
},
Capability::Secret { ids } => Capability::Secret {
ids: intersect_globs(ids, &host_lists(host, |c| secret_ids(c))),
},
Capability::Config { keys } => Capability::Config {
keys: intersect_globs(keys, &host_lists(host, |c| config_keys(c))),
},
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, |c| host_query_scopes(c));
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)))
}
}
#[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::*;
#[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 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"));
}
}