Skip to main content

open_agent_profile/
policy.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{Adjustment, AgentProfile, object, strings};
7
8/// Ordered permission decision used for fail-closed narrowing.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum PermissionDecision {
12    /// Operation is forbidden.
13    Deny,
14    /// Operation requires approval.
15    Ask,
16    /// Operation is allowed.
17    Allow,
18}
19
20/// Returns the more restrictive of a policy ceiling and requested decision.
21pub fn narrow_decision(
22    policy: PermissionDecision,
23    requested: PermissionDecision,
24) -> PermissionDecision {
25    std::cmp::min(policy, requested)
26}
27
28/// Effective tool set and explanations for removed grants.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct EffectiveTools {
31    /// Sorted, deduplicated tools allowed by both profile and harness.
32    pub tools: Vec<String>,
33    /// Explanations for requested tools that were removed.
34    pub adjustments: Vec<Adjustment>,
35}
36
37fn wildcard(pattern: &str, value: &str) -> bool {
38    if pattern == "*" {
39        return true;
40    }
41    if let Some(prefix) = pattern.strip_suffix('*') {
42        return value.starts_with(prefix);
43    }
44    pattern == value
45}
46
47/// Intersects harness-granted tools with an OAP profile's tool policy.
48pub fn intersect_tools(
49    profile: &AgentProfile,
50    granted: impl IntoIterator<Item = impl AsRef<str>>,
51) -> EffectiveTools {
52    let tools = object(object(profile.get("spec")).get("tools"));
53    let policy = tools
54        .get("policy")
55        .and_then(Value::as_str)
56        .unwrap_or("inherit");
57    let allow = strings(tools.get("allow"));
58    let deny = strings(tools.get("deny"));
59    let mut effective = vec![];
60    let mut adjustments = vec![];
61    for raw in granted {
62        let tool = raw.as_ref();
63        let requested = policy != "deny_all"
64            && (policy != "allowlist" || allow.iter().any(|item| wildcard(item, tool)))
65            && !deny.iter().any(|item| wildcard(item, tool));
66        if requested {
67            effective.push(tool.to_owned());
68        } else {
69            adjustments.push(Adjustment {
70                field: format!("tools.{tool}"),
71                requested: Value::String("allow".into()),
72                effective: Value::String("deny".into()),
73                reason: "profile and harness capabilities intersect; they never union".into(),
74            });
75        }
76    }
77    effective.sort();
78    effective.dedup();
79    EffectiveTools {
80        tools: effective,
81        adjustments,
82    }
83}
84
85/// Applies field-by-field harness ceilings to a requested permission map.
86pub fn narrow_permission_map(
87    requested: &std::collections::BTreeMap<String, PermissionDecision>,
88    policy: &std::collections::BTreeMap<String, PermissionDecision>,
89) -> (
90    std::collections::BTreeMap<String, PermissionDecision>,
91    Vec<Adjustment>,
92) {
93    let keys: BTreeSet<_> = requested.keys().chain(policy.keys()).cloned().collect();
94    let mut effective = std::collections::BTreeMap::new();
95    let mut adjustments = vec![];
96    for key in keys {
97        let ask = *requested.get(&key).unwrap_or(&PermissionDecision::Ask);
98        let ceiling = *policy.get(&key).unwrap_or(&PermissionDecision::Ask);
99        let value = narrow_decision(ceiling, ask);
100        effective.insert(key.clone(), value);
101        if value != ask {
102            adjustments.push(Adjustment {
103                field: key,
104                requested: serde_json::to_value(ask).unwrap(),
105                effective: serde_json::to_value(value).unwrap(),
106                reason: "harness policy is the upper bound".into(),
107            });
108        }
109    }
110    (effective, adjustments)
111}