use super::*;
use crate::orchestration::policy::external_roots::{governing_root, is_read_side};
use crate::orchestration::policy::ExternalRootAccess;
pub const SOURCE_DEFAULT_SENSITIVE_PATH: &str = "default_sensitive_path";
pub const SOURCE_DEFAULT_PATH_GUARD: &str = "default_path_guard";
pub const SOURCE_DEFAULT_EXTERNAL_PATH: &str = "default_external_path";
pub const EXTERNAL_ROOT_READ_ONLY: &str = "external_root_read_only";
pub const SOURCE_NET_POLICY: &str = "harn.net_policy";
pub(super) fn evaluate_context(
policy: &ToolApprovalPolicy,
ctx: &mut EvaluationContext,
) -> Option<PolicyEvaluation> {
ctx.external_roots =
super::super::external_roots::governing_roots(&policy.external_roots, &ctx.path_entries);
default_guard(policy, ctx).map(|candidate| evaluation_from_candidate(candidate, ctx))
}
pub fn denial_gate_for_source(source: Option<&str>) -> crate::agent_events::DenialGate {
use crate::agent_events::DenialGate;
match source {
Some("approval_unavailable") => DenialGate::ApprovalUnavailable,
Some(SOURCE_DEFAULT_SENSITIVE_PATH) => DenialGate::SensitivePath,
Some(SOURCE_NET_POLICY) => DenialGate::NetworkPolicy,
Some(SOURCE_DEFAULT_PATH_GUARD) | Some(SOURCE_DEFAULT_EXTERNAL_PATH) => {
DenialGate::WorkspaceBoundary
}
_ => DenialGate::ApprovalPolicy,
}
}
pub(super) fn default_guard(
policy: &ToolApprovalPolicy,
ctx: &EvaluationContext,
) -> Option<Candidate> {
if !policy.allow_sensitive_paths {
if let Some(path) = sensitive_paths::first_candidate(policy, &ctx.path_candidates) {
let path = sensitive_paths::bounded_evidence(&path);
return Some(Candidate {
source: SOURCE_DEFAULT_SENSITIVE_PATH.to_string(),
source_rank: PolicyRuleSource::Policy,
index: None,
id: Some("sensitive_path".to_string()),
action: PolicyAction::Deny,
reason: format!("path '{path}' is denied by the sensitive-path default"),
approval: ApprovalShape::default(),
risk_labels: vec!["sensitive_path".to_string()],
denied_paths: vec![path],
contributing_rules: Vec::new(),
});
}
}
for entry in &ctx.path_entries {
if !policy.allow_external_paths && matches!(entry.kind, WorkspacePathKind::Invalid) {
return Some(Candidate {
source: SOURCE_DEFAULT_PATH_GUARD.to_string(),
source_rank: PolicyRuleSource::Policy,
index: None,
id: Some("invalid_path".to_string()),
action: PolicyAction::Deny,
reason: entry
.reason
.clone()
.unwrap_or_else(|| format!("path '{}' is invalid", entry.display_path())),
approval: ApprovalShape::default(),
risk_labels: vec!["invalid_path".to_string()],
denied_paths: vec![entry.display_path().to_string()],
contributing_rules: Vec::new(),
});
}
if entry.workspace_path.is_some() {
continue;
}
let Some(host_path) = entry.host_path.as_deref() else {
continue;
};
if let Some(root) = governing_root(host_path, &policy.external_roots) {
if root.access == ExternalRootAccess::Read
&& !is_read_side(ctx.side_effect.as_deref(), ctx.tool_kind.as_deref())
{
return Some(Candidate {
source: SOURCE_DEFAULT_EXTERNAL_PATH.to_string(),
source_rank: PolicyRuleSource::Policy,
index: None,
id: Some(EXTERNAL_ROOT_READ_ONLY.to_string()),
action: PolicyAction::Deny,
reason: format!(
"path '{}' is under external root '{}' whose access is '{}'; \
tool '{}' may only read there",
entry.display_path(),
root.path,
root.access,
ctx.tool_name,
),
approval: ApprovalShape::default(),
risk_labels: vec![EXTERNAL_ROOT_READ_ONLY.to_string()],
denied_paths: vec![entry.display_path().to_string()],
contributing_rules: Vec::new(),
});
}
} else if !policy.allow_external_paths {
return Some(Candidate {
source: SOURCE_DEFAULT_EXTERNAL_PATH.to_string(),
source_rank: PolicyRuleSource::Policy,
index: None,
id: Some("external_path".to_string()),
action: PolicyAction::Deny,
reason: format!(
"path '{}' is outside the workspace and no external root allows it",
entry.display_path()
),
approval: ApprovalShape::default(),
risk_labels: vec!["external_path".to_string()],
denied_paths: vec![entry.display_path().to_string()],
contributing_rules: Vec::new(),
});
}
}
None
}
impl PolicyEvaluation {
pub fn denial_gate(&self) -> crate::agent_events::DenialGate {
denial_gate_for_source(self.matched_rule.as_ref().map(|rule| rule.source.as_str()))
}
pub fn terminal_denial(&self) -> crate::agent_events::ToolDenial {
let mut denial = crate::agent_events::ToolDenial::terminal(
self.denial_gate(),
None,
self.reason.clone(),
);
denial.denied_paths = self.denied_paths.clone();
denial.denied_network_targets = self.denied_network_targets.clone();
denial
}
pub fn is_approval_unavailable(&self) -> bool {
self.denial_gate() == crate::agent_events::DenialGate::ApprovalUnavailable
}
pub fn approval_unavailable_denial(
&self,
session: &str,
reason: impl Into<String>,
) -> crate::agent_events::ToolDenial {
let (class, repeat_count) =
super::next_approval_unavailable_class_repeat_count(session, &self.risk_labels);
crate::agent_events::ToolDenial::terminal(
crate::agent_events::DenialGate::ApprovalUnavailable,
None,
reason,
)
.with_denial_class(class, repeat_count)
}
}