use std::path::{Component as PathComponent, Path, PathBuf};
use crate::{
containment::{PreopenReport, PreopenSurface},
errors::RuntimeError,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NetworkAllowRule {
pub(crate) scheme: Option<String>,
pub(crate) host: String,
pub(crate) port: Option<u16>,
}
impl NetworkAllowRule {
pub(crate) fn parse(entry: &str) -> Result<Self, RuntimeError> {
let trimmed = entry.trim();
if trimmed.is_empty() {
return Err(RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: "entry must not be empty".to_string(),
});
}
if trimmed.contains("://") {
parse_url_allow_rule(trimmed)
} else {
parse_host_allow_rule(trimmed)
}
}
pub(crate) fn covers(&self, narrower: &NetworkAllowRule) -> bool {
if self.host != narrower.host {
return false;
}
if let Some(expected_scheme) = &self.scheme {
if narrower.scheme.as_ref() != Some(expected_scheme) {
return false;
}
}
match self.port {
Some(expected_port) => narrower.port == Some(expected_port),
None => true,
}
}
pub(crate) fn matches(&self, target: &RequestTarget) -> bool {
if self.host != target.host {
return false;
}
if let Some(expected_scheme) = &self.scheme {
if expected_scheme != &target.scheme {
return false;
}
}
match self.port {
Some(expected_port) => target.port == Some(expected_port),
None => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RequestTarget {
pub(crate) scheme: String,
pub(crate) host: String,
pub(crate) port: Option<u16>,
}
impl RequestTarget {
pub(crate) fn from_request(uri: &http::Uri, use_tls: bool) -> Option<Self> {
let authority = uri.authority()?;
let scheme = uri
.scheme_str()
.map(|s| s.to_ascii_lowercase())
.unwrap_or_else(|| if use_tls { "https" } else { "http" }.to_string());
let port = authority
.port_u16()
.or_else(|| default_port_for_scheme(&scheme));
Some(Self {
scheme,
host: authority.host().to_ascii_lowercase(),
port,
})
}
}
pub(crate) fn parse_network_allow_rules(
entries: &[String],
) -> Result<Vec<NetworkAllowRule>, RuntimeError> {
entries
.iter()
.map(|entry| NetworkAllowRule::parse(entry))
.collect()
}
fn parse_url_allow_rule(entry: &str) -> Result<NetworkAllowRule, RuntimeError> {
let url = url::Url::parse(entry).map_err(|err| RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: format!("invalid URL: {err}"),
})?;
let scheme = url.scheme().to_ascii_lowercase();
if scheme != "http" && scheme != "https" {
return Err(RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: format!("unsupported URL scheme '{scheme}' (expected http or https)"),
});
}
if url.query().is_some() || url.fragment().is_some() || (url.path() != "" && url.path() != "/")
{
return Err(RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: "URL allow entries must not include path, query, or fragment".to_string(),
});
}
let host = url
.host_str()
.ok_or_else(|| RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: "URL allow entries must include a host".to_string(),
})?
.to_ascii_lowercase();
let port = url.port().or_else(|| default_port_for_scheme(&scheme));
Ok(NetworkAllowRule {
scheme: Some(scheme),
host,
port,
})
}
fn parse_host_allow_rule(entry: &str) -> Result<NetworkAllowRule, RuntimeError> {
let prefixed = format!("http://{entry}");
let parsed =
url::Url::parse(&prefixed).map_err(|err| RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: format!("invalid host allow entry: {err}"),
})?;
if parsed.path() != "/" || parsed.query().is_some() || parsed.fragment().is_some() {
return Err(RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: "host allow entries must be host or host:port only".to_string(),
});
}
let host = parsed
.host_str()
.ok_or_else(|| RuntimeError::InvalidNetworkAllowEntry {
entry: entry.to_string(),
message: "host allow entries must include a host".to_string(),
})?
.to_ascii_lowercase();
Ok(NetworkAllowRule {
scheme: None,
host,
port: parsed.port(),
})
}
fn default_port_for_scheme(scheme: &str) -> Option<u16> {
match scheme {
"http" => Some(80),
"https" => Some(443),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WorkdirSubpathRejection {
Absolute,
Escapes,
Prefixed,
}
impl WorkdirSubpathRejection {
pub(crate) fn message(self, noun: &str) -> String {
match self {
Self::Absolute => format!("{noun} must be relative to the workdir"),
Self::Escapes => format!("{noun} cannot escape the workdir via '..'"),
Self::Prefixed => format!("{noun} must not contain absolute or prefixed components"),
}
}
}
pub(crate) fn lower_workdir_subpath(raw: &str) -> Result<PathBuf, WorkdirSubpathRejection> {
let path = Path::new(raw);
if path.is_absolute() {
return Err(WorkdirSubpathRejection::Absolute);
}
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
PathComponent::CurDir => {}
PathComponent::Normal(part) => normalized.push(part),
PathComponent::ParentDir => {
if !normalized.pop() {
return Err(WorkdirSubpathRejection::Escapes);
}
}
PathComponent::Prefix(_) | PathComponent::RootDir => {
return Err(WorkdirSubpathRejection::Prefixed);
}
}
}
Ok(normalized)
}
pub(crate) fn validate_filesystem_scope(scope: &str) -> Result<(), RuntimeError> {
lower_workdir_subpath(scope)
.map(|_| ())
.map_err(|rejection| RuntimeError::InvalidFilesystemScope {
scope: scope.to_string(),
message: rejection.message("scope"),
})
}
fn declared_filesystem_scope(
capabilities: &murmur_artifact::Capabilities,
) -> Result<Option<String>, RuntimeError> {
let scope = capabilities
.filesystem
.as_ref()
.and_then(|filesystem| filesystem.scope.clone());
if let Some(scope) = scope.as_deref() {
validate_filesystem_scope(scope)?;
}
Ok(scope)
}
fn declares_records(capabilities: &murmur_artifact::Capabilities) -> bool {
capabilities
.filesystem
.as_ref()
.is_some_and(|filesystem| filesystem.records)
}
pub(crate) fn resolve_scoped_dir(root: &Path, scope: &str) -> Result<PathBuf, RuntimeError> {
let scoped_dir = root.join(scope);
std::fs::create_dir_all(&scoped_dir).map_err(|err| {
RuntimeError::wasi(
scoped_dir.clone(),
format!("failed to create granted filesystem scope '{scope}': {err}"),
)
})?;
Ok(scoped_dir)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct HookCapabilityGrant {
pub(crate) network_allow_rules: Vec<NetworkAllowRule>,
pub(crate) filesystem_scope: Option<String>,
pub(crate) filesystem_records: bool,
pub(crate) records_hooks_dir: Option<PathBuf>,
pub(crate) task_io_read: bool,
pub(crate) conversation_read: bool,
pub(crate) state_store: Option<String>,
pub(crate) state_dir: Option<PathBuf>,
pub(crate) config_json: Option<String>,
}
impl HookCapabilityGrant {
pub(crate) fn derive(
capabilities: Option<&murmur_artifact::Capabilities>,
capsule_name: &str,
) -> Result<Self, RuntimeError> {
let Some(capabilities) = capabilities else {
return Ok(Self::default());
};
let network_allow_rules = match capabilities.network.as_ref() {
Some(network) => parse_network_allow_rules(&network.allow)?,
None => Vec::new(),
};
let filesystem_scope = declared_filesystem_scope(capabilities)?;
let filesystem_records = declares_records(capabilities);
Ok(Self {
network_allow_rules,
filesystem_scope,
filesystem_records,
records_hooks_dir: None,
task_io_read: capabilities
.task_io
.as_ref()
.is_some_and(|task_io| task_io.read),
conversation_read: capabilities
.conversation
.as_ref()
.is_some_and(|conversation| conversation.read),
state_store: derive_state_store(capabilities, capsule_name)?,
state_dir: None,
config_json: None,
})
}
}
fn derive_state_store(
capabilities: &murmur_artifact::Capabilities,
capsule_name: &str,
) -> Result<Option<String>, RuntimeError> {
let Some(state) = capabilities.state.as_ref() else {
return Ok(None);
};
let store = crate::state_store::resolve_store_name(state, capsule_name);
crate::state_store::validate_store_name(&store)?;
Ok(Some(store))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct ToolCapabilityGrant {
pub(crate) network_allow_rules: Option<Vec<NetworkAllowRule>>,
pub(crate) filesystem_scope: Option<String>,
pub(crate) dropped_network_entries: Vec<String>,
pub(crate) state_store: Option<String>,
pub(crate) state_dir: Option<PathBuf>,
pub(crate) config_json: Option<String>,
}
impl ToolCapabilityGrant {
pub(crate) fn derive(
capabilities: Option<&murmur_artifact::Capabilities>,
ceiling_network_allow_rules: &[NetworkAllowRule],
capsule_name: &str,
) -> Result<Self, RuntimeError> {
let Some(capabilities) = capabilities else {
return Ok(Self::default());
};
let mut dropped_network_entries = Vec::new();
let network_allow_rules = match capabilities.network.as_ref() {
None => None,
Some(network) => {
let mut kept = Vec::new();
for entry in &network.allow {
let rule = NetworkAllowRule::parse(entry)?;
if ceiling_network_allow_rules
.iter()
.any(|ceiling_rule| ceiling_rule.covers(&rule))
{
kept.push(rule);
} else {
dropped_network_entries.push(entry.clone());
}
}
Some(kept)
}
};
let filesystem_scope = declared_filesystem_scope(capabilities)?;
Ok(Self {
network_allow_rules,
filesystem_scope,
dropped_network_entries,
state_store: derive_state_store(capabilities, capsule_name)?,
state_dir: None,
config_json: None,
})
}
}
pub fn preopen_reports<'a, I>(artifacts: I) -> Result<Vec<PreopenReport>, RuntimeError>
where
I: IntoIterator<
Item = (
&'a str,
&'a murmur_artifact::ArtifactRuntime,
Option<&'a murmur_artifact::Capabilities>,
),
>,
{
let mut reports = Vec::new();
for (name, runtime, capabilities) in artifacts {
if matches!(runtime, murmur_artifact::ArtifactRuntime::Skill) {
continue;
}
let scope = capabilities
.map(declared_filesystem_scope)
.transpose()?
.flatten();
let records = matches!(runtime, murmur_artifact::ArtifactRuntime::Hook)
&& capabilities.is_some_and(declares_records);
let surface = match (runtime, scope.is_some()) {
(_, true) => PreopenSurface::ScopedSubtree,
(murmur_artifact::ArtifactRuntime::Hook, false) if records => {
PreopenSurface::RecordsHooks
}
(murmur_artifact::ArtifactRuntime::Hook, false) => PreopenSurface::Nothing,
(_, false) => PreopenSurface::WholeWorkdir,
};
let runtime_dir_reach =
crate::runtime_dir::gets_runtime_dir_preopen(runtime, scope.is_some())
.then_some(crate::containment::RuntimeDirReach::Unresolved);
reports.push(PreopenReport {
artifact: name.to_string(),
role: runtime.as_str().to_string(),
scope,
surface,
runtime_dir_reach,
});
}
Ok(reports)
}
pub(crate) fn effective_tool_network_rules<'a>(
grant: Option<&'a ToolCapabilityGrant>,
ceiling_network_allow_rules: &'a [NetworkAllowRule],
) -> &'a [NetworkAllowRule] {
grant
.and_then(|grant| grant.network_allow_rules.as_deref())
.unwrap_or(ceiling_network_allow_rules)
}
#[cfg(test)]
mod tests {
use super::*;
use murmur_artifact::{Capabilities, FilesystemCapabilities, NetworkCapabilities};
fn capabilities_block(
network: Option<Vec<&str>>,
filesystem_scope: Option<&str>,
) -> Capabilities {
Capabilities {
peer_fetch: None,
network: network.map(|allow| NetworkCapabilities {
allow: allow.into_iter().map(str::to_string).collect(),
unix_sockets: false,
}),
filesystem: filesystem_scope.map(|scope| FilesystemCapabilities {
scope: Some(scope.to_string()),
workdir_exec: false,
read_only: Vec::new(),
records: false,
}),
shell: None,
spawn: None,
env: None,
limits: None,
resources: None,
state: None,
task_io: None,
conversation: None,
plan: None,
install: None,
containment: None,
}
}
#[test]
fn hook_grant_defaults_to_deny_network_and_filesystem() {
let grant = HookCapabilityGrant::derive(None, "test-capsule").unwrap();
assert_eq!(grant, HookCapabilityGrant::default());
assert!(grant.network_allow_rules.is_empty());
assert!(grant.filesystem_scope.is_none());
let target = RequestTarget {
scheme: "https".to_string(),
host: "telemetry.example.com".to_string(),
port: Some(443),
};
assert!(!grant
.network_allow_rules
.iter()
.any(|rule| rule.matches(&target)));
}
#[test]
fn hook_grant_empty_capabilities_block_grants_nothing() {
let grant =
HookCapabilityGrant::derive(Some(&capabilities_block(None, None)), "test-capsule")
.unwrap();
assert_eq!(grant, HookCapabilityGrant::default());
}
#[test]
fn hook_grant_network_allows_exactly_the_declared_host() {
let caps = capabilities_block(Some(vec!["https://telemetry.example.com"]), None);
let grant = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap();
let allowed = RequestTarget {
scheme: "https".to_string(),
host: "telemetry.example.com".to_string(),
port: Some(443),
};
let other = RequestTarget {
scheme: "https".to_string(),
host: "evil.example.com".to_string(),
port: Some(443),
};
assert!(grant
.network_allow_rules
.iter()
.any(|rule| rule.matches(&allowed)));
assert!(!grant
.network_allow_rules
.iter()
.any(|rule| rule.matches(&other)));
assert!(grant.filesystem_scope.is_none());
}
#[test]
fn hook_grant_filesystem_scope_is_carried_through() {
let caps = capabilities_block(None, Some("hook-state"));
let grant = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap();
assert_eq!(grant.filesystem_scope.as_deref(), Some("hook-state"));
assert!(grant.network_allow_rules.is_empty());
}
#[test]
fn hook_grant_task_io_read_is_carried_through_independently() {
for (declared, expected) in [(None, false), (Some(false), false), (Some(true), true)] {
let caps = murmur_artifact::Capabilities {
task_io: declared.map(|read| murmur_artifact::TaskIoCapabilities { read }),
conversation: None,
plan: None,
..capabilities_block(None, None)
};
let grant = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap();
assert_eq!(grant.task_io_read, expected, "declared: {declared:?}");
assert!(grant.network_allow_rules.is_empty());
assert!(grant.filesystem_scope.is_none());
}
}
#[test]
fn hook_grant_conversation_read_is_carried_through_independently() {
for (declared, expected) in [(None, false), (Some(false), false), (Some(true), true)] {
let caps = murmur_artifact::Capabilities {
conversation: declared
.map(|read| murmur_artifact::ConversationCapabilities { read }),
..capabilities_block(None, None)
};
let grant = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap();
assert_eq!(grant.conversation_read, expected, "declared: {declared:?}");
assert!(!grant.task_io_read, "one grant never implies the other");
assert!(grant.network_allow_rules.is_empty());
assert!(grant.filesystem_scope.is_none());
}
}
#[test]
fn state_store_defaults_to_the_capsule_name_and_an_explicit_name_wins() {
for (declared, expected) in [(None, "state-capsule"), (Some("shey"), "shey")] {
let caps = Capabilities {
state: Some(murmur_artifact::StateCapabilities {
store: declared.map(str::to_string),
}),
..capabilities_block(None, None)
};
let hook = HookCapabilityGrant::derive(Some(&caps), "state-capsule").unwrap();
let tool =
ToolCapabilityGrant::derive(Some(&caps), &ceiling(), "state-capsule").unwrap();
assert_eq!(hook.state_store.as_deref(), Some(expected));
assert_eq!(tool.state_store.as_deref(), Some(expected));
assert!(hook.state_dir.is_none());
assert!(tool.state_dir.is_none());
assert!(hook.filesystem_scope.is_none());
assert!(hook.network_allow_rules.is_empty());
assert!(tool.filesystem_scope.is_none());
assert!(tool.network_allow_rules.is_none());
}
}
#[test]
fn an_absent_state_block_grants_no_store_to_either_role() {
let caps = capabilities_block(Some(vec!["https://api.example.com"]), Some("cache"));
assert!(HookCapabilityGrant::derive(Some(&caps), "state-capsule")
.unwrap()
.state_store
.is_none());
assert!(
ToolCapabilityGrant::derive(Some(&caps), &ceiling(), "state-capsule")
.unwrap()
.state_store
.is_none()
);
}
#[test]
fn a_malformed_store_name_fails_lowering_for_both_roles() {
for store in ["../escape", "/abs/path", "a/b", "", ".hidden"] {
let caps = Capabilities {
state: Some(murmur_artifact::StateCapabilities {
store: Some(store.to_string()),
}),
..capabilities_block(None, None)
};
for err in [
HookCapabilityGrant::derive(Some(&caps), "state-capsule").unwrap_err(),
ToolCapabilityGrant::derive(Some(&caps), &ceiling(), "state-capsule").unwrap_err(),
] {
assert!(
matches!(err, RuntimeError::InvalidStateStore { .. }),
"store '{store}' must refuse as InvalidStateStore, got: {err}"
);
}
}
}
#[test]
fn an_empty_capsule_name_cannot_become_a_store_name() {
let caps = Capabilities {
state: Some(murmur_artifact::StateCapabilities { store: None }),
..capabilities_block(None, None)
};
assert!(matches!(
ToolCapabilityGrant::derive(Some(&caps), &ceiling(), ""),
Err(RuntimeError::InvalidStateStore { .. })
));
}
#[test]
fn hook_grant_rejects_escaping_filesystem_scope() {
for scope in ["../escape", "/etc"] {
let caps = capabilities_block(None, Some(scope));
let err = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidFilesystemScope { .. }),
"scope {scope} should fail staging, got: {err}"
);
}
}
#[test]
fn hook_grant_rejects_malformed_network_entry() {
let caps = capabilities_block(Some(vec!["ftp://files.example.com"]), None);
let err = HookCapabilityGrant::derive(Some(&caps), "test-capsule").unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidNetworkAllowEntry { .. }),
"got: {err}"
);
}
fn ceiling() -> Vec<NetworkAllowRule> {
parse_network_allow_rules(&[
"https://api.example.com".to_string(),
"https://other.example.com".to_string(),
])
.unwrap()
}
fn target(host: &str) -> RequestTarget {
RequestTarget {
scheme: "https".to_string(),
host: host.to_string(),
port: Some(443),
}
}
fn reaches(rules: &[NetworkAllowRule], host: &str) -> bool {
rules.iter().any(|rule| rule.matches(&target(host)))
}
#[test]
fn tool_grant_without_entry_inherits_the_ceiling_unchanged() {
let ceiling = ceiling();
let grant = ToolCapabilityGrant::derive(None, &ceiling, "test-capsule").unwrap();
assert_eq!(grant, ToolCapabilityGrant::default());
assert!(grant.network_allow_rules.is_none());
assert!(grant.filesystem_scope.is_none());
assert!(grant.dropped_network_entries.is_empty());
let effective = effective_tool_network_rules(Some(&grant), &ceiling);
assert_eq!(effective, ceiling.as_slice());
assert!(reaches(effective, "api.example.com"));
assert!(reaches(effective, "other.example.com"));
}
#[test]
fn tool_grant_subset_of_ceiling_is_kept_and_narrows() {
let ceiling = ceiling();
let caps = capabilities_block(Some(vec!["https://api.example.com"]), None);
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
let effective = effective_tool_network_rules(Some(&grant), &ceiling);
assert!(reaches(effective, "api.example.com"));
assert!(!reaches(effective, "other.example.com"));
assert!(grant.dropped_network_entries.is_empty());
}
#[test]
fn tool_grant_entry_outside_the_ceiling_is_dropped_not_granted() {
let ceiling = ceiling();
let caps = capabilities_block(
Some(vec!["https://api.example.com", "https://evil.example.com"]),
None,
);
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
let effective = effective_tool_network_rules(Some(&grant), &ceiling);
assert!(reaches(effective, "api.example.com"));
assert!(!reaches(effective, "evil.example.com"));
assert_eq!(
grant.dropped_network_entries,
vec!["https://evil.example.com".to_string()]
);
}
#[test]
fn tool_grant_entry_broader_than_the_ceiling_rule_is_dropped() {
let ceiling = ceiling();
let caps = capabilities_block(Some(vec!["api.example.com"]), None);
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
assert_eq!(grant.network_allow_rules.as_deref(), Some(&[][..]));
assert_eq!(
grant.dropped_network_entries,
vec!["api.example.com".to_string()]
);
}
#[test]
fn tool_grant_empty_network_allow_denies_all_for_that_artifact() {
let ceiling = ceiling();
let caps = capabilities_block(Some(vec![]), None);
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
let effective = effective_tool_network_rules(Some(&grant), &ceiling);
assert!(effective.is_empty());
assert!(!reaches(effective, "api.example.com"));
assert!(!reaches(effective, "other.example.com"));
}
#[test]
fn tool_grant_filesystem_scope_is_carried_and_leaves_network_inherited() {
let ceiling = ceiling();
let caps = capabilities_block(None, Some("cache"));
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
assert_eq!(grant.filesystem_scope.as_deref(), Some("cache"));
assert!(grant.network_allow_rules.is_none());
assert_eq!(
effective_tool_network_rules(Some(&grant), &ceiling),
ceiling.as_slice()
);
}
#[test]
fn tool_grant_rejects_escaping_filesystem_scope() {
for scope in ["../escape", "/etc"] {
let caps = capabilities_block(None, Some(scope));
let err =
ToolCapabilityGrant::derive(Some(&caps), &ceiling(), "test-capsule").unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidFilesystemScope { .. }),
"scope {scope} should fail staging, got: {err}"
);
}
}
#[test]
fn tool_grant_rejects_malformed_network_entry() {
let caps = capabilities_block(Some(vec!["ftp://files.example.com"]), None);
let err = ToolCapabilityGrant::derive(Some(&caps), &ceiling(), "test-capsule").unwrap_err();
assert!(
matches!(err, RuntimeError::InvalidNetworkAllowEntry { .. }),
"got: {err}"
);
}
#[test]
fn tool_grant_cannot_widen_an_empty_ceiling() {
let caps = capabilities_block(Some(vec!["https://api.example.com"]), None);
let grant = ToolCapabilityGrant::derive(Some(&caps), &[], "test-capsule").unwrap();
assert_eq!(grant.network_allow_rules.as_deref(), Some(&[][..]));
assert_eq!(
grant.dropped_network_entries,
vec!["https://api.example.com".to_string()]
);
}
#[test]
fn ceiling_rule_with_wildcard_scheme_covers_a_scheme_bound_rule() {
let ceiling = parse_network_allow_rules(&["api.example.com".to_string()]).unwrap();
let caps = capabilities_block(Some(vec!["https://api.example.com"]), None);
let grant = ToolCapabilityGrant::derive(Some(&caps), &ceiling, "test-capsule").unwrap();
assert!(grant.dropped_network_entries.is_empty());
assert!(reaches(
effective_tool_network_rules(Some(&grant), &ceiling),
"api.example.com"
));
}
#[test]
fn filesystem_scope_validation_accepts_safe_relative_paths() {
for scope in [".", "./workdir", "a/../b", "sandbox/subdir"] {
assert!(
validate_filesystem_scope(scope).is_ok(),
"scope should be valid: {scope}"
);
}
}
#[test]
fn filesystem_scope_validation_rejects_escape_paths() {
for scope in ["../x", "../../x", "/tmp"] {
assert!(
validate_filesystem_scope(scope).is_err(),
"scope should be invalid: {scope}"
);
}
}
#[test]
fn network_allowlist_empty_denies_all_requests() {
let rules = parse_network_allow_rules(&Vec::<String>::new()).unwrap();
let target = RequestTarget {
scheme: "https".to_string(),
host: "blocked.example.com".to_string(),
port: Some(443),
};
assert!(!rules.iter().any(|rule| rule.matches(&target)));
}
#[test]
fn network_allowlist_matches_allowed_host_with_default_port() {
let rules =
parse_network_allow_rules(&["https://allowed.example.com".to_string()]).unwrap();
let target = RequestTarget {
scheme: "https".to_string(),
host: "allowed.example.com".to_string(),
port: Some(443),
};
assert!(rules.iter().any(|rule| rule.matches(&target)));
}
#[test]
fn network_allowlist_blocks_non_matching_host() {
let rules =
parse_network_allow_rules(&["https://allowed.example.com".to_string()]).unwrap();
let target = RequestTarget {
scheme: "https".to_string(),
host: "blocked.example.com".to_string(),
port: Some(443),
};
assert!(!rules.iter().any(|rule| rule.matches(&target)));
}
#[test]
fn network_allowlist_normalizes_default_ports() {
let rules = parse_network_allow_rules(&[
"https://allowed.example.com".to_string(),
"allowed-two.example.com:8080".to_string(),
])
.unwrap();
let https_default = RequestTarget {
scheme: "https".to_string(),
host: "allowed.example.com".to_string(),
port: Some(443),
};
let custom_port = RequestTarget {
scheme: "http".to_string(),
host: "allowed-two.example.com".to_string(),
port: Some(8080),
};
assert!(rules.iter().any(|rule| rule.matches(&https_default)));
assert!(rules.iter().any(|rule| rule.matches(&custom_port)));
}
#[test]
fn undeclared_entries_report_the_baseline_their_role_starts_from() {
let tool = murmur_artifact::ArtifactRuntime::Tool;
let driver = murmur_artifact::ArtifactRuntime::Driver;
let hook = murmur_artifact::ArtifactRuntime::Hook;
let reports = preopen_reports(vec![
("notes-tool", &tool, None),
("anthropic-driver", &driver, None),
("telemetry-hook", &hook, None),
])
.unwrap();
assert_eq!(
reports,
vec![
PreopenReport {
artifact: "notes-tool".to_string(),
role: "tool".to_string(),
scope: None,
surface: PreopenSurface::WholeWorkdir,
runtime_dir_reach: Some(crate::containment::RuntimeDirReach::Unresolved),
},
PreopenReport {
artifact: "anthropic-driver".to_string(),
role: "driver".to_string(),
scope: None,
surface: PreopenSurface::WholeWorkdir,
runtime_dir_reach: None,
},
PreopenReport {
artifact: "telemetry-hook".to_string(),
role: "hook".to_string(),
scope: None,
surface: PreopenSurface::Nothing,
runtime_dir_reach: None,
},
]
);
}
#[test]
fn a_declared_scope_narrows_every_role_to_a_subtree() {
let tool = murmur_artifact::ArtifactRuntime::Tool;
let hook = murmur_artifact::ArtifactRuntime::Hook;
let tool_caps = capabilities_block(None, Some("cache"));
let hook_caps = capabilities_block(None, Some("hook-state"));
let reports = preopen_reports(vec![
("notes-tool", &tool, Some(&tool_caps)),
("telemetry-hook", &hook, Some(&hook_caps)),
])
.unwrap();
assert_eq!(
reports,
vec![
PreopenReport {
artifact: "notes-tool".to_string(),
role: "tool".to_string(),
scope: Some("cache".to_string()),
surface: PreopenSurface::ScopedSubtree,
runtime_dir_reach: None,
},
PreopenReport {
artifact: "telemetry-hook".to_string(),
role: "hook".to_string(),
scope: Some("hook-state".to_string()),
surface: PreopenSurface::ScopedSubtree,
runtime_dir_reach: None,
},
]
);
}
#[test]
fn a_skill_produces_no_entry() {
let skill = murmur_artifact::ArtifactRuntime::Skill;
let tool = murmur_artifact::ArtifactRuntime::Tool;
let reports = preopen_reports(vec![
("notes-skill", &skill, None),
("notes-tool", &tool, None),
])
.unwrap();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].artifact, "notes-tool");
}
#[test]
fn an_escaping_scope_is_refused() {
let tool = murmur_artifact::ArtifactRuntime::Tool;
let caps = capabilities_block(None, Some("../escape"));
let err = preopen_reports(vec![("notes-tool", &tool, Some(&caps))]).unwrap_err();
assert!(
matches!(
err,
RuntimeError::InvalidFilesystemScope { ref scope, .. } if scope == "../escape"
),
"unexpected error: {err}"
);
assert!(
ToolCapabilityGrant::derive(Some(&caps), &[], "test-capsule").is_err(),
"the grant derivation must refuse what the report refuses"
);
}
#[test]
fn preopen_surfaces_serialize_to_stable_wire_names() {
for (surface, wire) in [
(PreopenSurface::WholeWorkdir, "whole-workdir"),
(PreopenSurface::ScopedSubtree, "scoped-subtree"),
(PreopenSurface::Nothing, "nothing"),
] {
assert_eq!(
serde_json::to_value(surface).unwrap(),
serde_json::Value::String(wire.to_string())
);
}
}
}