open_agent_profile/
policy.rs1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{Adjustment, AgentProfile, object, strings};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum PermissionDecision {
12 Deny,
14 Ask,
16 Allow,
18}
19
20pub fn narrow_decision(
22 policy: PermissionDecision,
23 requested: PermissionDecision,
24) -> PermissionDecision {
25 std::cmp::min(policy, requested)
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct EffectiveTools {
31 pub tools: Vec<String>,
33 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
47pub 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
85pub 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}