use std::collections::BTreeSet;
use std::fmt;
use std::path::{Component, Path};
use murmur_artifact::{ContainmentClass, RuntimeManifest};
use crate::network_policy::{validate_filesystem_scope, NetworkAllowRule};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvelopeAxis {
NetworkAllow,
UnixSockets,
PeerFetchAllow,
ShellAllow,
SpawnAllow,
EnvAllow,
FilesystemScope,
WorkdirExec,
StateStore,
InstallSkill,
InstallTool,
InstallFrom,
Containment,
}
impl EnvelopeAxis {
pub fn manifest_key(self) -> &'static str {
match self {
Self::NetworkAllow => "capabilities.network.allow",
Self::UnixSockets => "capabilities.network.unix_sockets",
Self::PeerFetchAllow => "capabilities.peer_fetch.allow",
Self::ShellAllow => "capabilities.shell.allow",
Self::SpawnAllow => "capabilities.spawn.allow",
Self::EnvAllow => "capabilities.env.allow",
Self::FilesystemScope => "capabilities.filesystem.scope",
Self::WorkdirExec => "capabilities.filesystem.workdir_exec",
Self::StateStore => "capabilities.state.store",
Self::InstallSkill => "capabilities.install.skill",
Self::InstallTool => "capabilities.install.tool",
Self::InstallFrom => "capabilities.install.from",
Self::Containment => "capabilities.containment",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvelopeViolation {
pub axis: EnvelopeAxis,
pub entry: String,
pub parent_entry: Option<String>,
}
impl fmt::Display for EnvelopeViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let key = self.axis.manifest_key();
let parent = self.parent_entry.as_deref().unwrap_or_default();
match self.axis {
EnvelopeAxis::Containment => write!(
f,
"{key}: the child declares containment '{}', below its parent's floor '{parent}' \
— a spawned capsule's containment floor may only rise",
self.entry,
),
EnvelopeAxis::FilesystemScope if self.entry.is_empty() => write!(
f,
"{key}: the child declares no scope and would reach the whole workdir, which its \
parent's scope '{parent}' does not cover — a spawned capsule can never hold more \
capability than the capsule that spawned it",
),
EnvelopeAxis::FilesystemScope => write!(
f,
"{key}: the child declares '{}', which its parent's scope '{parent}' does not \
cover — a spawned capsule can never hold more capability than the capsule that \
spawned it",
self.entry,
),
_ => write!(
f,
"{key}: the child declares '{}', which its parent does not hold — a spawned \
capsule can never hold more capability than the capsule that spawned it",
self.entry,
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SpawnEnvelope {
pub network_allow: Vec<String>,
pub unix_sockets_allowed: bool,
pub peer_fetch_allow: Vec<String>,
pub shell_allow: Vec<String>,
pub spawn_allow: Vec<String>,
pub env_allow: Vec<String>,
pub filesystem_scope: Option<String>,
pub workdir_exec_allowed: bool,
pub state_stores: BTreeSet<String>,
pub install_skill: Vec<String>,
pub install_tool: Vec<String>,
pub install_from_sources: bool,
pub containment_floor: ContainmentClass,
}
impl SpawnEnvelope {
pub fn from_runtime_manifest(manifest: &RuntimeManifest) -> Self {
let policy = crate::types::capability_policy_from_runtime_manifest(manifest);
Self {
network_allow: policy.network_allow,
unix_sockets_allowed: policy.unix_sockets_allowed,
peer_fetch_allow: policy.peer_fetch_allow,
shell_allow: policy.shell_allow,
spawn_allow: policy.spawn_allow,
env_allow: policy.env_allow,
filesystem_scope: policy.filesystem_scope,
workdir_exec_allowed: policy.workdir_exec_allowed,
state_stores: declared_state_stores(manifest),
install_skill: policy.install_skill,
install_tool: policy.install_tool,
install_from_sources: policy.install_from_sources,
containment_floor: policy.containment_floor,
}
}
pub fn contains(&self, child: &Self) -> Result<(), EnvelopeViolation> {
if let Some(entry) = first_uncovered_host(&self.network_allow, &child.network_allow) {
return Err(violation(EnvelopeAxis::NetworkAllow, entry));
}
if child.unix_sockets_allowed && !self.unix_sockets_allowed {
return Err(violation(EnvelopeAxis::UnixSockets, "true".to_string()));
}
if let Some(entry) = first_uncovered_host(&self.peer_fetch_allow, &child.peer_fetch_allow) {
return Err(violation(EnvelopeAxis::PeerFetchAllow, entry));
}
if let Some(entry) = first_absent(&self.shell_allow, &child.shell_allow) {
return Err(violation(EnvelopeAxis::ShellAllow, entry));
}
if let Some(entry) = first_absent(&self.spawn_allow, &child.spawn_allow) {
return Err(violation(EnvelopeAxis::SpawnAllow, entry));
}
if let Some(entry) = first_absent(&self.env_allow, &child.env_allow) {
return Err(violation(EnvelopeAxis::EnvAllow, entry));
}
if let Some(parent_scope) = self.filesystem_scope.as_deref() {
let child_scope = child.filesystem_scope.as_deref().unwrap_or_default();
if !scope_covers(parent_scope, child_scope) {
return Err(EnvelopeViolation {
axis: EnvelopeAxis::FilesystemScope,
entry: child_scope.to_string(),
parent_entry: Some(parent_scope.to_string()),
});
}
}
if child.workdir_exec_allowed && !self.workdir_exec_allowed {
return Err(violation(EnvelopeAxis::WorkdirExec, "true".to_string()));
}
if let Some(store) = child
.state_stores
.difference(&self.state_stores)
.next()
.cloned()
{
return Err(violation(EnvelopeAxis::StateStore, store));
}
if let Some(entry) = first_uncovered_install(&self.install_skill, &child.install_skill) {
return Err(violation(EnvelopeAxis::InstallSkill, entry));
}
if let Some(entry) = first_uncovered_install(&self.install_tool, &child.install_tool) {
return Err(violation(EnvelopeAxis::InstallTool, entry));
}
if child.install_from_sources && !self.install_from_sources {
return Err(violation(EnvelopeAxis::InstallFrom, "sources".to_string()));
}
if child.containment_floor < self.containment_floor {
return Err(EnvelopeViolation {
axis: EnvelopeAxis::Containment,
entry: child.containment_floor.to_string(),
parent_entry: Some(self.containment_floor.to_string()),
});
}
Ok(())
}
}
fn violation(axis: EnvelopeAxis, entry: String) -> EnvelopeViolation {
EnvelopeViolation {
axis,
entry,
parent_entry: None,
}
}
fn declared_state_stores(manifest: &RuntimeManifest) -> BTreeSet<String> {
manifest
.artifacts
.iter()
.filter_map(|artifact| artifact.capabilities.as_ref())
.filter_map(|capabilities| capabilities.state.as_ref())
.map(|state| crate::state_store::resolve_store_name(state, &manifest.name))
.collect()
}
fn first_uncovered_host(ceiling: &[String], candidate: &[String]) -> Option<String> {
let ceiling_rules: Vec<NetworkAllowRule> = ceiling
.iter()
.filter_map(|entry| NetworkAllowRule::parse(entry).ok())
.collect();
candidate
.iter()
.find(|entry| match NetworkAllowRule::parse(entry) {
Ok(rule) => !ceiling_rules
.iter()
.any(|ceiling_rule| ceiling_rule.covers(&rule)),
Err(_) => true,
})
.cloned()
}
fn first_absent(ceiling: &[String], candidate: &[String]) -> Option<String> {
candidate
.iter()
.find(|entry| !ceiling.contains(entry))
.cloned()
}
fn first_uncovered_install(ceiling: &[String], candidate: &[String]) -> Option<String> {
candidate
.iter()
.find(|entry| {
!ceiling
.iter()
.any(|parent| murmur_artifact::install_pattern::covers(parent, entry))
})
.cloned()
}
fn scope_covers(parent: &str, child: &str) -> bool {
match (scope_components(parent), scope_components(child)) {
(Some(parent), Some(child)) => child.starts_with(&parent),
_ => false,
}
}
fn scope_components(scope: &str) -> Option<Vec<String>> {
if scope.is_empty() {
return Some(Vec::new());
}
validate_filesystem_scope(scope).ok()?;
let mut components = Vec::new();
for component in Path::new(scope).components() {
match component {
Component::CurDir => {}
Component::Normal(part) => components.push(part.to_string_lossy().into_owned()),
Component::ParentDir => {
components.pop();
}
Component::Prefix(_) | Component::RootDir => return None,
}
}
Some(components)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network_policy::{parse_network_allow_rules, ToolCapabilityGrant};
const COVERAGE_CASES: &[(&str, &str, bool)] = &[
("example.com", "https://example.com", true),
("https://example.com", "example.com", false),
("example.com", "example.com", true),
("https://example.com", "https://example.com", true),
("https://example.com", "http://example.com", false),
("example.com:8443", "example.com:8443", true),
("example.com:8443", "example.com:9000", false),
("example.com", "other.example.com", false),
];
fn envelope(capabilities_yaml: &str) -> SpawnEnvelope {
manifest_envelope("cap", capabilities_yaml)
}
fn manifest_envelope(name: &str, capabilities_yaml: &str) -> SpawnEnvelope {
let manifest = RuntimeManifest::from_yaml_str(&format!(
"name: {name}\nversion: 0.1.0\n{capabilities_yaml}"
))
.expect("manifest fixture must parse");
SpawnEnvelope::from_runtime_manifest(&manifest)
}
fn network_envelope(entries: &[&str]) -> SpawnEnvelope {
SpawnEnvelope {
network_allow: entries.iter().map(|entry| entry.to_string()).collect(),
..SpawnEnvelope::default()
}
}
#[test]
fn the_narrowing_path_and_the_spawn_path_share_one_subset_rule() {
for (ceiling, candidate, expected) in COVERAGE_CASES {
let ceiling_rules = parse_network_allow_rules(&[ceiling.to_string()])
.expect("ceiling fixture must parse");
let capabilities = murmur_artifact::Capabilities {
network: Some(murmur_artifact::NetworkCapabilities {
allow: vec![candidate.to_string()],
unix_sockets: false,
}),
peer_fetch: None,
filesystem: None,
shell: None,
spawn: None,
env: None,
limits: None,
resources: None,
state: None,
task_io: None,
conversation: None,
plan: None,
install: None,
containment: None,
};
let narrowed = ToolCapabilityGrant::derive(Some(&capabilities), &ceiling_rules, "cap")
.expect("candidate fixture must parse");
let narrowing_verdict = narrowed.dropped_network_entries.is_empty();
let spawn_verdict = network_envelope(&[ceiling])
.contains(&network_envelope(&[candidate]))
.is_ok();
assert_eq!(
narrowing_verdict, spawn_verdict,
"'{ceiling}' vs '{candidate}': the narrowing path and the spawn path disagree",
);
assert_eq!(
spawn_verdict, *expected,
"'{ceiling}' vs '{candidate}': expected covered={expected}",
);
}
}
#[test]
fn a_network_host_the_parent_does_not_hold_is_refused_by_name() {
let parent = envelope("capabilities:\n network:\n allow: [registry.internal]\n");
let child = envelope("capabilities:\n network:\n allow: [api.example.com]\n");
let violation = parent.contains(&child).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::NetworkAllow);
assert_eq!(violation.entry, "api.example.com");
let message = violation.to_string();
assert!(message.contains("capabilities.network.allow"), "{message}");
assert!(message.contains("api.example.com"), "{message}");
assert!(message.contains("its parent does not hold"), "{message}");
}
#[test]
fn every_axis_refuses_naming_its_own_manifest_key_and_entry() {
let parent_yaml = "capabilities:\n \
network:\n allow: [registry.internal]\n unix_sockets: false\n \
peer_fetch:\n allow: [peer.internal]\n \
shell:\n allow: [git]\n \
spawn:\n allow: [local/worker-a]\n \
env:\n allow: [HOME]\n \
filesystem:\n scope: data\n workdir_exec: false\n \
install:\n skill: [\"local/style-*\"]\n tool: [local/jq]\n";
let parent = envelope(parent_yaml);
let cases: &[(&str, EnvelopeAxis, &str)] = &[
(
"capabilities:\n network:\n allow: [api.example.com]\n",
EnvelopeAxis::NetworkAllow,
"api.example.com",
),
(
"capabilities:\n network:\n unix_sockets: true\n",
EnvelopeAxis::UnixSockets,
"true",
),
(
"capabilities:\n peer_fetch:\n allow: [other.internal]\n",
EnvelopeAxis::PeerFetchAllow,
"other.internal",
),
(
"capabilities:\n shell:\n allow: [curl]\n",
EnvelopeAxis::ShellAllow,
"curl",
),
(
"capabilities:\n spawn:\n allow: [local/worker-b]\n",
EnvelopeAxis::SpawnAllow,
"local/worker-b",
),
(
"capabilities:\n env:\n allow: [GITHUB_TOKEN]\n",
EnvelopeAxis::EnvAllow,
"GITHUB_TOKEN",
),
(
"capabilities:\n filesystem:\n scope: other\n",
EnvelopeAxis::FilesystemScope,
"other",
),
(
"capabilities:\n filesystem:\n scope: data/../other\n",
EnvelopeAxis::FilesystemScope,
"data/../other",
),
(
"capabilities:\n filesystem:\n scope: data\n workdir_exec: true\n",
EnvelopeAxis::WorkdirExec,
"true",
),
(
"capabilities:\n filesystem:\n scope: data\n install:\n skill: [\"*\"]\n",
EnvelopeAxis::InstallSkill,
"*",
),
(
"capabilities:\n filesystem:\n scope: data\n install:\n tool: [local/yq]\n",
EnvelopeAxis::InstallTool,
"local/yq",
),
];
for (child_yaml, axis, entry) in cases {
let violation = parent.contains(&envelope(child_yaml)).unwrap_err();
assert_eq!(violation.axis, *axis, "child: {child_yaml}");
assert_eq!(violation.entry, *entry, "child: {child_yaml}");
let message = violation.to_string();
assert!(message.contains(axis.manifest_key()), "{message}");
assert!(message.contains(entry), "{message}");
}
}
#[test]
fn a_child_install_entry_fits_only_under_a_covering_parent_entry() {
let parent = envelope("capabilities:\n install:\n skill: [\"local/style-*\"]\n");
for (skill, covered) in [
("local/style-rust", true),
("\"local/style-rust-*\"", true),
("\"local/style-*\"", true),
("\"local/style*\"", false),
("\"ana/style-rust\"", false),
("\"*\"", false),
("local/stylist", false),
] {
let child = envelope(&format!(
"capabilities:\n install:\n skill: [{skill}]\n"
));
let verdict = parent.contains(&child);
assert_eq!(verdict.is_ok(), covered, "child skill {skill}: {verdict:?}");
if let Err(violation) = verdict {
assert_eq!(violation.axis, EnvelopeAxis::InstallSkill);
assert_eq!(violation.entry, skill.trim_matches('"'));
}
}
let child_tool = envelope("capabilities:\n install:\n tool: [local/style-rust]\n");
let violation = parent.contains(&child_tool).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::InstallTool);
assert_eq!(violation.entry, "local/style-rust");
}
#[test]
fn a_child_fetching_from_sources_is_refused_under_a_parent_that_does_not() {
let stores = envelope("capabilities:\n install:\n tool: [\"*\"]\n");
let sources =
envelope("capabilities:\n install:\n tool: [\"*\"]\n from: [stores, sources]\n");
assert!(!stores.install_from_sources && sources.install_from_sources);
let violation = stores.contains(&sources).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::InstallFrom);
assert_eq!(violation.entry, "sources");
assert!(
violation.to_string().starts_with(
"capabilities.install.from: the child declares 'sources', which its parent does \
not hold"
),
"{violation}"
);
assert_eq!(sources.contains(&stores), Ok(()));
assert_eq!(sources.contains(&sources), Ok(()));
}
#[test]
fn a_child_declaring_no_install_is_within_any_parent() {
let parent = envelope("capabilities:\n install:\n skill: [\"*\"]\n tool: [\"*\"]\n");
let child = envelope("capabilities:\n env:\n allow: []\n");
assert!(child.install_skill.is_empty() && child.install_tool.is_empty());
assert_eq!(parent.contains(&child), Ok(()));
}
#[test]
fn a_child_declaring_no_scope_is_refused_under_a_scoped_parent() {
let parent = envelope("capabilities:\n filesystem:\n scope: data\n");
let child = envelope("capabilities:\n env:\n allow: []\n");
let violation = parent.contains(&child).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::FilesystemScope);
assert!(violation.entry.is_empty());
let message = violation.to_string();
assert!(
message.contains("capabilities.filesystem.scope"),
"{message}"
);
assert!(message.contains("declares no scope"), "{message}");
assert!(message.contains("'data'"), "{message}");
}
#[test]
fn a_scope_at_or_beneath_the_parents_is_within_the_envelope() {
let parent = envelope("capabilities:\n filesystem:\n scope: data\n");
for scope in ["data", "data/in", "data/in/../in"] {
let child = envelope(&format!(
"capabilities:\n filesystem:\n scope: {scope}\n"
));
assert!(parent.contains(&child).is_ok(), "scope '{scope}'");
}
}
#[test]
fn an_unscoped_parent_covers_any_child_scope() {
let parent = envelope("capabilities:\n env:\n allow: []\n");
let child = envelope("capabilities:\n filesystem:\n scope: data\n");
assert!(parent.contains(&child).is_ok());
}
#[test]
fn a_state_store_the_parents_artifacts_do_not_open_is_refused() {
let with_state = |name: &str, store: &str| {
manifest_envelope(
name,
&format!(
"artifacts:\n - name: local/writer\n version: 0.1.0\n runtime: tool\n \
capabilities:\n state:\n store: {store}\n"
),
)
};
let parent = with_state("parent", "parent-notes");
let child = with_state("child", "child-notes");
let violation = parent.contains(&child).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::StateStore);
assert_eq!(violation.entry, "child-notes");
assert!(violation.to_string().contains("capabilities.state.store"));
let shared = with_state("child", "parent-notes");
assert!(parent.contains(&shared).is_ok());
}
#[test]
fn an_undeclared_store_resolves_to_the_capsule_name() {
let envelope = manifest_envelope(
"notes-capsule",
"artifacts:\n - name: local/writer\n version: 0.1.0\n runtime: tool\n \
capabilities:\n state: {}\n",
);
assert_eq!(
envelope.state_stores.iter().collect::<Vec<_>>(),
vec!["notes-capsule"]
);
}
#[test]
fn a_containment_floor_may_rise_but_never_fall() {
let parent = envelope("capabilities:\n containment: scoped\n");
for raised in ["scoped", "sealed"] {
let child = envelope(&format!("capabilities:\n containment: {raised}\n"));
assert!(parent.contains(&child).is_ok(), "floor '{raised}'");
}
let child = envelope("capabilities:\n containment: advisory\n");
let violation = parent.contains(&child).unwrap_err();
assert_eq!(violation.axis, EnvelopeAxis::Containment);
assert_eq!(violation.entry, "advisory");
let message = violation.to_string();
assert!(message.contains("capabilities.containment"), "{message}");
assert!(message.contains("advisory"), "{message}");
assert!(message.contains("may only rise"), "{message}");
}
#[test]
fn a_refusal_names_the_key_the_declaration_and_the_rule() {
let list_axis = envelope("capabilities:\n network:\n allow: [registry.internal]\n")
.contains(&envelope(
"capabilities:\n network:\n allow: [api.example.com]\n",
))
.unwrap_err();
assert_eq!(
list_axis.to_string(),
"capabilities.network.allow: the child declares 'api.example.com', which its parent \
does not hold — a spawned capsule can never hold more capability than the capsule \
that spawned it",
);
let absent_scope = envelope("capabilities:\n filesystem:\n scope: data\n")
.contains(&envelope("artifacts: []\n"))
.unwrap_err();
assert_eq!(
absent_scope.to_string(),
"capabilities.filesystem.scope: the child declares no scope and would reach the whole \
workdir, which its parent's scope 'data' does not cover — a spawned capsule can never \
hold more capability than the capsule that spawned it",
);
let floor = envelope("capabilities:\n containment: scoped\n")
.contains(&envelope("capabilities:\n containment: advisory\n"))
.unwrap_err();
assert_eq!(
floor.to_string(),
"capabilities.containment: the child declares containment 'advisory', below its \
parent's floor 'scoped' — a spawned capsule's containment floor may only rise",
);
}
#[test]
fn a_silent_child_is_within_any_parent() {
let parent = envelope(
"capabilities:\n network:\n allow: [registry.internal]\n \
shell:\n allow: [git]\n",
);
assert!(parent.contains(&envelope("artifacts: []\n")).is_ok());
assert!(envelope("artifacts: []\n")
.contains(&envelope("artifacts: []\n"))
.is_ok());
}
}