use std::collections::HashSet;
use std::sync::{
Arc,
atomic::{AtomicU8, Ordering},
};
use parking_lot::RwLock;
use crate::SkillTrustLevel;
use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::permissions::{AutonomyLevel, PermissionAction, PermissionPolicy};
use crate::registry::ToolDef;
pub use zeph_common::quarantine::QUARANTINE_DENIED;
pub(crate) use zeph_common::quarantine::is_quarantine_denied;
pub(crate) fn quarantine_denial_message(tool_id: &str, active_skills: &[String]) -> String {
if active_skills.is_empty() {
format!("{tool_id} denied (trust=quarantined)")
} else {
format!(
"{tool_id} denied: this turn's active skill set {active_skills:?} has a combined \
trust floor of quarantined (weakest-link policy over all co-active skills this \
turn; this reflects the turn's overall trust floor and may not be about the \
specific tool/skill you targeted)"
)
}
}
pub(crate) fn trust_to_u8(level: SkillTrustLevel) -> u8 {
match level {
SkillTrustLevel::Trusted => 0,
SkillTrustLevel::Verified => 1,
SkillTrustLevel::Quarantined => 2,
_ => 3,
}
}
pub(crate) fn u8_to_trust(v: u8) -> SkillTrustLevel {
match v {
0 => SkillTrustLevel::Trusted,
1 => SkillTrustLevel::Verified,
2 => SkillTrustLevel::Quarantined,
_ => SkillTrustLevel::Blocked,
}
}
pub struct TrustGateExecutor<T: ToolExecutor> {
inner: T,
policy: PermissionPolicy,
effective_trust: AtomicU8,
mcp_tool_ids: Arc<RwLock<HashSet<String>>>,
}
impl<T: ToolExecutor + std::fmt::Debug> std::fmt::Debug for TrustGateExecutor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustGateExecutor")
.field("inner", &self.inner)
.field("policy", &self.policy)
.field("effective_trust", &self.effective_trust())
.field("mcp_tool_ids", &self.mcp_tool_ids)
.finish()
}
}
impl<T: ToolExecutor> TrustGateExecutor<T> {
#[must_use]
pub fn new(inner: T, policy: PermissionPolicy) -> Self {
Self {
inner,
policy,
effective_trust: AtomicU8::new(trust_to_u8(SkillTrustLevel::Trusted)),
mcp_tool_ids: Arc::new(RwLock::new(HashSet::new())),
}
}
#[must_use]
pub fn mcp_tool_ids_handle(&self) -> Arc<RwLock<HashSet<String>>> {
Arc::clone(&self.mcp_tool_ids)
}
pub fn set_effective_trust(&self, level: SkillTrustLevel) {
self.effective_trust
.store(trust_to_u8(level), Ordering::Relaxed);
}
#[must_use]
pub fn effective_trust(&self) -> SkillTrustLevel {
u8_to_trust(self.effective_trust.load(Ordering::Relaxed))
}
fn is_mcp_tool(&self, tool_id: &str) -> bool {
self.mcp_tool_ids.read().contains(tool_id)
}
fn check_trust(
&self,
tool_id: &str,
input: &str,
active_skills: &[String],
) -> Result<(), ToolError> {
match self.effective_trust() {
SkillTrustLevel::Blocked => {
return Err(ToolError::Blocked {
command: "all tools blocked (trust=blocked)".to_owned(),
});
}
SkillTrustLevel::Quarantined
if is_quarantine_denied(tool_id) || self.is_mcp_tool(tool_id) =>
{
return Err(ToolError::Blocked {
command: quarantine_denial_message(tool_id, active_skills),
});
}
_ => {}
}
if self.policy.autonomy_level() == AutonomyLevel::Supervised
&& self.policy.rules().get(tool_id).is_none()
&& (self.is_mcp_tool(tool_id) || crate::permissions::is_readonly_tool(tool_id))
{
return Ok(());
}
match self.policy.check(tool_id, input) {
PermissionAction::Allow => Ok(()),
PermissionAction::Ask => Err(ToolError::ConfirmationRequired {
command: input.to_owned(),
}),
_ => Err(ToolError::Blocked {
command: input.to_owned(),
}),
}
}
}
impl<T: ToolExecutor> ToolExecutor for TrustGateExecutor<T> {
async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
match self.effective_trust() {
SkillTrustLevel::Blocked | SkillTrustLevel::Quarantined => {
return Err(ToolError::Blocked {
command: format!(
"tool execution denied (trust={})",
format!("{:?}", self.effective_trust()).to_lowercase()
),
});
}
_ => {}
}
self.inner.execute(response).await
}
async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
match self.effective_trust() {
SkillTrustLevel::Blocked | SkillTrustLevel::Quarantined => {
return Err(ToolError::Blocked {
command: format!(
"tool execution denied (trust={})",
format!("{:?}", self.effective_trust()).to_lowercase()
),
});
}
_ => {}
}
self.inner.execute_confirmed(response).await
}
fn tool_definitions(&self) -> Vec<ToolDef> {
self.inner.tool_definitions()
}
async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
let input = call
.params
.get("command")
.or_else(|| call.params.get("file_path"))
.or_else(|| call.params.get("query"))
.or_else(|| call.params.get("url"))
.or_else(|| call.params.get("uri"))
.and_then(|v| v.as_str())
.unwrap_or("");
self.check_trust(
call.tool_id.as_str(),
input,
call.skill_name.as_deref().unwrap_or(&[]),
)?;
self.inner.execute_tool_call(call).await
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
match self.effective_trust() {
SkillTrustLevel::Blocked => {
return Err(ToolError::Blocked {
command: "all tools blocked (trust=blocked)".to_owned(),
});
}
SkillTrustLevel::Quarantined
if is_quarantine_denied(call.tool_id.as_str())
|| self.is_mcp_tool(call.tool_id.as_str()) =>
{
return Err(ToolError::Blocked {
command: quarantine_denial_message(
call.tool_id.as_str(),
call.skill_name.as_deref().unwrap_or(&[]),
),
});
}
_ => {}
}
self.inner.execute_tool_call_confirmed(call).await
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
self.inner.set_skill_env(env);
}
fn is_tool_retryable(&self, tool_id: &str) -> bool {
self.inner.is_tool_retryable(tool_id)
}
fn is_tool_speculatable(&self, tool_id: &str) -> bool {
self.inner.is_tool_speculatable(tool_id)
}
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
self.inner.checkpoint_undo(n)
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
self.inner.checkpoint_redo()
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
self.inner.checkpoint_list()
}
fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
self.effective_trust
.store(trust_to_u8(level), Ordering::Relaxed);
}
fn requires_confirmation(&self, call: &crate::executor::ToolCall) -> bool {
let input = call
.params
.get("command")
.or_else(|| call.params.get("file_path"))
.or_else(|| call.params.get("query"))
.or_else(|| call.params.get("url"))
.or_else(|| call.params.get("uri"))
.and_then(|v| v.as_str())
.unwrap_or("");
matches!(
self.check_trust(
call.tool_id.as_str(),
input,
call.skill_name.as_deref().unwrap_or(&[]),
),
Err(ToolError::ConfirmationRequired { .. })
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[derive(Debug)]
struct MockExecutor;
impl ToolExecutor for MockExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
Ok(Some(ToolOutput {
tool_name: call.tool_id.clone(),
summary: "ok".into(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
crate::tool_executor_no_inner_defaults!();
}
fn make_call(tool_id: &str) -> ToolCall {
ToolCall {
tool_id: tool_id.into(),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
}
}
fn make_call_with_cmd(tool_id: &str, cmd: &str) -> ToolCall {
let mut params = serde_json::Map::new();
params.insert("command".into(), serde_json::Value::String(cmd.into()));
ToolCall {
tool_id: tool_id.into(),
params,
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
}
}
fn make_call_with_skills(tool_id: &str, skills: &[&str]) -> ToolCall {
ToolCall {
tool_id: tool_id.into(),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: Some(skills.iter().map(ToString::to_string).collect()),
}
}
fn blocked_command(result: Result<Option<ToolOutput>, ToolError>) -> String {
match result {
Err(ToolError::Blocked { command }) => command,
other => panic!("expected Err(ToolError::Blocked {{ .. }}), got {other:?}"),
}
}
#[tokio::test]
async fn supervised_readonly_native_tool_without_rule_allowed() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate.execute_tool_call(&make_call("read")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn supervised_unconfigured_non_mcp_non_readonly_tool_requires_confirmation() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate.execute_tool_call(&make_call("bash")).await;
assert_matches!(result, Err(ToolError::ConfirmationRequired { .. }));
}
#[tokio::test]
async fn supervised_unconfigured_diagnostics_requires_confirmation() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate.execute_tool_call(&make_call("diagnostics")).await;
assert_matches!(result, Err(ToolError::ConfirmationRequired { .. }));
}
#[tokio::test]
async fn quarantined_denies_bash() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("bash")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_write() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("write")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_edit() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("edit")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_delete_path() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("delete_path")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_fetch() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("fetch")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_memory_save() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("memory_save")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_diagnostics() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("diagnostics")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_allows_read() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("read")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn quarantined_allows_file_read() {
let mut rules = std::collections::HashMap::new();
rules.insert(
"file_read".to_owned(),
vec![crate::permissions::PermissionRule {
pattern: "*".to_owned(),
action: PermissionAction::Allow,
}],
);
let policy = crate::permissions::PermissionPolicy::new(rules);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("file_read")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn blocked_denies_everything() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Blocked);
let result = gate.execute_tool_call(&make_call("file_read")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn policy_deny_overrides_trust() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate
.execute_tool_call(&make_call_with_cmd("bash", "sudo rm"))
.await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn blocked_denies_execute() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Blocked);
let result = gate.execute("some response").await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn blocked_denies_execute_confirmed() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Blocked);
let result = gate.execute_confirmed("some response").await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn trusted_allows_execute() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate.execute("some response").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn verified_with_allow_policy_succeeds() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Verified);
let result = gate
.execute_tool_call(&make_call_with_cmd("bash", "echo hi"))
.await
.unwrap();
assert!(result.is_some());
}
#[tokio::test]
async fn quarantined_denies_web_scrape() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("web_scrape")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denial_message_names_active_skills_not_target_tool() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let call =
make_call_with_skills("invoke_skill", &["disk-usage", "persona-customer-support"]);
let result = gate.execute_tool_call(&call).await;
let message = blocked_command(result);
assert!(
message.contains("disk-usage") && message.contains("persona-customer-support"),
"message should name the actual active skills, got: {message}"
);
assert_ne!(
message, "invoke_skill denied (trust=quarantined)",
"message must not read as if invoke_skill itself is the untrusted party"
);
}
#[tokio::test]
async fn quarantined_denial_message_unchanged_when_no_active_skills() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("invoke_skill")).await;
let message = blocked_command(result);
assert_eq!(message, "invoke_skill denied (trust=quarantined)");
}
#[tokio::test]
async fn quarantined_denial_message_unchanged_when_active_skills_empty() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call(&make_call_with_skills("invoke_skill", &[]))
.await;
let message = blocked_command(result);
assert_eq!(message, "invoke_skill denied (trust=quarantined)");
}
#[tokio::test]
async fn quarantined_denial_message_names_active_skills_confirmed_path() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let call =
make_call_with_skills("invoke_skill", &["disk-usage", "persona-customer-support"]);
let result = gate.execute_tool_call_confirmed(&call).await;
let message = blocked_command(result);
assert!(
message.contains("disk-usage") && message.contains("persona-customer-support"),
"confirmed path message should name the actual active skills, got: {message}"
);
assert_ne!(
message, "invoke_skill denied (trust=quarantined)",
"confirmed path message must not read as if invoke_skill itself is untrusted"
);
}
#[tokio::test]
async fn quarantined_denial_message_names_active_skills_for_non_skill_tool() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let call = make_call_with_skills("bash", &["disk-usage", "persona-customer-support"]);
let result = gate.execute_tool_call(&call).await;
let message = blocked_command(result);
assert!(
message.contains("disk-usage") && message.contains("persona-customer-support"),
"message for a non-skill tool should also name the active skills, got: {message}"
);
assert_ne!(message, "bash denied (trust=quarantined)");
}
#[derive(Debug)]
struct EnvCapture {
captured: std::sync::Mutex<Option<std::collections::HashMap<String, String>>>,
}
impl EnvCapture {
fn new() -> Self {
Self {
captured: std::sync::Mutex::new(None),
}
}
}
impl ToolExecutor for EnvCapture {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(&self, _: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
*self.captured.lock().unwrap() = env;
}
crate::tool_executor_no_inner_defaults!();
}
#[test]
fn is_tool_retryable_delegated_to_inner() {
#[derive(Debug)]
struct RetryableExecutor;
impl ToolExecutor for RetryableExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
_: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn is_tool_retryable(&self, tool_id: &str) -> bool {
tool_id == "fetch"
}
crate::tool_executor_no_inner_defaults!();
}
let gate = TrustGateExecutor::new(RetryableExecutor, PermissionPolicy::default());
assert!(gate.is_tool_retryable("fetch"));
assert!(!gate.is_tool_retryable("bash"));
}
#[test]
fn checkpoint_methods_delegated_to_inner() {
#[derive(Debug)]
struct CheckpointingExecutor;
impl ToolExecutor for CheckpointingExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
_: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
crate::executor::CheckpointActionResult {
supported: true,
message: "stub".into(),
reverted_commands: n,
..Default::default()
}
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
crate::executor::CheckpointActionResult {
supported: true,
message: "stub".into(),
..Default::default()
}
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
crate::executor::CheckpointListResult {
supported: true,
..Default::default()
}
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
self.execute_tool_call(call).await
}
fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
false
}
fn requires_confirmation(&self, _call: &ToolCall) -> bool {
false
}
}
let gate = TrustGateExecutor::new(CheckpointingExecutor, PermissionPolicy::default());
let undo_result = gate.checkpoint_undo(7);
assert!(undo_result.supported);
assert_eq!(
undo_result.reverted_commands, 7,
"n must be forwarded, not hardcoded"
);
assert!(gate.checkpoint_redo().supported);
assert!(gate.checkpoint_list().supported);
}
#[test]
fn set_skill_env_forwarded_to_inner() {
let inner = EnvCapture::new();
let gate = TrustGateExecutor::new(inner, PermissionPolicy::default());
let mut env = std::collections::HashMap::new();
env.insert("MY_VAR".to_owned(), "42".to_owned());
gate.set_skill_env(Some(env.clone()));
let captured = gate.inner.captured.lock().unwrap();
assert_eq!(*captured, Some(env));
}
#[tokio::test]
async fn mcp_tool_supervised_no_rules_allows() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Trusted);
gate.mcp_tool_ids_handle()
.write()
.insert("mcp_filesystem__read_file".to_owned());
let mut params = serde_json::Map::new();
params.insert(
"file_path".into(),
serde_json::Value::String("/tmp/test.txt".into()),
);
let call = ToolCall {
tool_id: "mcp_filesystem__read_file".into(),
params,
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = gate.execute_tool_call(&call).await;
assert!(
result.is_ok(),
"MCP tool should be allowed when no rules exist"
);
}
#[tokio::test]
async fn bash_with_explicit_deny_rule_blocked() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&["sudo".into()], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate
.execute_tool_call(&make_call_with_cmd("bash", "sudo apt install vim"))
.await;
assert!(
matches!(result, Err(ToolError::Blocked { .. })),
"bash with explicit deny rule should be blocked"
);
}
#[tokio::test]
async fn bash_with_explicit_allow_rule_succeeds() {
let policy = crate::permissions::PermissionPolicy::from_legacy(&[], &[]);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate
.execute_tool_call(&make_call_with_cmd("bash", "echo hello"))
.await;
assert!(
result.is_ok(),
"bash with explicit allow rule should succeed"
);
}
#[tokio::test]
async fn readonly_denies_mcp_tool_not_in_allowlist() {
let policy =
crate::permissions::PermissionPolicy::default().with_autonomy(AutonomyLevel::ReadOnly);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate
.execute_tool_call(&make_call("mcpls_get_diagnostics"))
.await;
assert!(
matches!(result, Err(ToolError::Blocked { .. })),
"ReadOnly mode must deny non-allowlisted tools"
);
}
#[test]
fn set_effective_trust_interior_mutability() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
assert_eq!(gate.effective_trust(), SkillTrustLevel::Quarantined);
gate.set_effective_trust(SkillTrustLevel::Blocked);
assert_eq!(gate.effective_trust(), SkillTrustLevel::Blocked);
gate.set_effective_trust(SkillTrustLevel::Trusted);
assert_eq!(gate.effective_trust(), SkillTrustLevel::Trusted);
}
#[test]
fn is_quarantine_denied_exact_match() {
assert!(is_quarantine_denied("bash"));
assert!(is_quarantine_denied("write"));
assert!(is_quarantine_denied("fetch"));
assert!(is_quarantine_denied("memory_save"));
assert!(is_quarantine_denied("delete_path"));
assert!(is_quarantine_denied("create_directory"));
assert!(is_quarantine_denied("diagnostics"));
}
#[test]
fn is_quarantine_denied_suffix_match_mcp_write() {
assert!(is_quarantine_denied("filesystem_write"));
assert!(!is_quarantine_denied("filesystem_write_file"));
}
#[test]
fn is_quarantine_denied_suffix_mcp_bash() {
assert!(is_quarantine_denied("shell_bash"));
assert!(is_quarantine_denied("mcp_shell_bash"));
}
#[test]
fn is_quarantine_denied_suffix_mcp_fetch() {
assert!(is_quarantine_denied("http_fetch"));
assert!(!is_quarantine_denied("server_prefetch"));
}
#[test]
fn is_quarantine_denied_suffix_mcp_memory_save() {
assert!(is_quarantine_denied("server_memory_save"));
assert!(!is_quarantine_denied("server_save"));
}
#[test]
fn is_quarantine_denied_suffix_mcp_delete_path() {
assert!(is_quarantine_denied("fs_delete_path"));
assert!(is_quarantine_denied("fs_not_delete_path"));
}
#[test]
fn is_quarantine_denied_substring_not_suffix() {
assert!(!is_quarantine_denied("write_log"));
}
#[test]
fn is_quarantine_denied_read_only_tools_allowed() {
assert!(!is_quarantine_denied("filesystem_read_file"));
assert!(!is_quarantine_denied("filesystem_list_dir"));
assert!(!is_quarantine_denied("read"));
assert!(!is_quarantine_denied("file_read"));
}
#[tokio::test]
async fn quarantined_denies_mcp_write_tool() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("filesystem_write")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_allows_mcp_read_file() {
let mut rules = std::collections::HashMap::new();
rules.insert(
"filesystem_read_file".to_owned(),
vec![crate::permissions::PermissionRule {
pattern: "*".to_owned(),
action: PermissionAction::Allow,
}],
);
let policy = crate::permissions::PermissionPolicy::new(rules);
let gate = TrustGateExecutor::new(MockExecutor, policy);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call(&make_call("filesystem_read_file"))
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn quarantined_denies_mcp_bash_tool() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("shell_bash")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_mcp_memory_save() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call(&make_call("server_memory_save"))
.await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_mcp_confirmed_path() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call_confirmed(&make_call("filesystem_write"))
.await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
fn gate_with_mcp_ids(ids: &[&str]) -> TrustGateExecutor<MockExecutor> {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
let handle = gate.mcp_tool_ids_handle();
let set: std::collections::HashSet<String> = ids.iter().map(ToString::to_string).collect();
*handle.write() = set;
gate
}
#[tokio::test]
async fn quarantined_denies_registered_mcp_tool_novel_name() {
let gate = gate_with_mcp_ids(&["github_run_command"]);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call(&make_call("github_run_command"))
.await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_denies_registered_mcp_tool_execute() {
let gate = gate_with_mcp_ids(&["shell_execute"]);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("shell_execute")).await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn quarantined_allows_unregistered_tool_not_in_denied_list() {
let gate = gate_with_mcp_ids(&["other_tool"]);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate.execute_tool_call(&make_call("read")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn trusted_allows_registered_mcp_tool() {
let gate = gate_with_mcp_ids(&["github_run_command"]);
gate.set_effective_trust(SkillTrustLevel::Trusted);
let result = gate
.execute_tool_call(&make_call("github_run_command"))
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn quarantined_denies_mcp_tool_via_confirmed_path() {
let gate = gate_with_mcp_ids(&["docker_container_exec"]);
gate.set_effective_trust(SkillTrustLevel::Quarantined);
let result = gate
.execute_tool_call_confirmed(&make_call("docker_container_exec"))
.await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[test]
fn mcp_tool_ids_handle_shared_arc() {
let gate = TrustGateExecutor::new(MockExecutor, PermissionPolicy::default());
let handle = gate.mcp_tool_ids_handle();
handle.write().insert("test_tool".to_owned());
assert!(gate.is_mcp_tool("test_tool"));
assert!(!gate.is_mcp_tool("other_tool"));
}
#[test]
fn invoke_skill_and_load_skill_suffix_match_is_intentional() {
assert!(is_quarantine_denied("invoke_skill"));
assert!(is_quarantine_denied("load_skill"));
assert!(is_quarantine_denied("foo_invoke_skill"));
assert!(is_quarantine_denied("foo_load_skill"));
}
}