kaynine_runtime/
policy.rs1use kaynine_core::error::PolicyError;
8use kaynine_core::policy::{Decision, Policy, PolicyRequest};
9use kaynine_core::store::SessionStore;
10use std::sync::Arc;
11
12pub struct PolicyChain {
18 pub layers: Vec<Arc<dyn Policy>>,
19}
20
21impl PolicyChain {
22 pub fn new(layers: Vec<Arc<dyn Policy>>) -> Self {
23 Self { layers }
24 }
25}
26
27#[async_trait::async_trait]
28impl Policy for PolicyChain {
29 async fn evaluate(&self, request: PolicyRequest) -> Result<Decision, PolicyError> {
30 let mut first_ask: Option<Decision> = None;
31 for layer in &self.layers {
32 match layer.evaluate(request.clone()).await? {
33 Decision::Deny { reason } => return Ok(Decision::Deny { reason }),
34 Decision::Ask => {
35 first_ask.get_or_insert(Decision::Ask);
36 }
37 Decision::Allow => {}
38 }
39 }
40 Ok(first_ask.unwrap_or(Decision::Allow))
41 }
42}
43
44pub struct GrantPolicy {
53 pub store: Arc<dyn SessionStore>,
54 pub namespace: String,
55}
56
57#[async_trait::async_trait]
58impl Policy for GrantPolicy {
59 async fn evaluate(&self, request: PolicyRequest) -> Result<Decision, PolicyError> {
60 let grants = self
61 .store
62 .list_grants(&self.namespace)
63 .await
64 .map_err(|e| PolicyError::Evaluate(e.to_string()))?;
65 let call_caps: Vec<serde_json::Value> = request
66 .call
67 .capabilities
68 .iter()
69 .map(serde_json::to_value)
70 .collect::<Result<_, _>>()
71 .map_err(|e| PolicyError::Evaluate(e.to_string()))?;
72 for grant in &grants {
73 let Some(granted) = grant.get("capability") else {
74 continue;
75 };
76 if call_caps.iter().any(|cap| cap == granted) {
77 return Ok(Decision::Allow);
78 }
79 }
80 Ok(Decision::Ask)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use kaynine_core::ids::{RunId, SessionId, ToolCallId};
89 use kaynine_core::policy::DenyAllPolicy;
90 use kaynine_core::store::SessionStore;
91 use kaynine_core::testing::ScriptedPolicy;
92 use kaynine_core::tool::{Capability, PreparedToolCall};
93 use kaynine_store::sqlite::SqliteStore;
94 use std::collections::VecDeque;
95 use std::sync::Mutex;
96
97 struct FixedPolicy(Decision);
98
99 #[async_trait::async_trait]
100 impl Policy for FixedPolicy {
101 async fn evaluate(&self, _request: PolicyRequest) -> Result<Decision, PolicyError> {
102 Ok(self.0.clone())
103 }
104 }
105
106 fn request_with_caps(caps: Vec<Capability>) -> PolicyRequest {
107 PolicyRequest {
108 call: PreparedToolCall {
109 call_id: ToolCallId::from("call-1"),
110 name: "bash".into(),
111 arguments: serde_json::json!({}),
112 capabilities: caps,
113 },
114 session_id: SessionId::from("s"),
115 run_id: RunId::from("r"),
116 turn: 1,
117 }
118 }
119
120 #[tokio::test]
121 async fn policy_chain_priority() {
122 let chain = PolicyChain::new(vec![
124 Arc::new(FixedPolicy(Decision::Ask)),
125 Arc::new(DenyAllPolicy {
126 reason: "机密".into(),
127 }),
128 ]);
129 assert_eq!(
130 chain.evaluate(request_with_caps(vec![])).await.unwrap(),
131 Decision::Deny {
132 reason: "机密".into()
133 }
134 );
135
136 let chain = PolicyChain::new(vec![
138 Arc::new(FixedPolicy(Decision::Allow)),
139 Arc::new(FixedPolicy(Decision::Ask)),
140 Arc::new(FixedPolicy(Decision::Allow)),
141 ]);
142 assert_eq!(
143 chain.evaluate(request_with_caps(vec![])).await.unwrap(),
144 Decision::Ask
145 );
146
147 let chain = PolicyChain::new(vec![
149 Arc::new(FixedPolicy(Decision::Allow)),
150 Arc::new(FixedPolicy(Decision::Allow)),
151 ]);
152 assert_eq!(
153 chain.evaluate(request_with_caps(vec![])).await.unwrap(),
154 Decision::Allow
155 );
156 }
157
158 #[tokio::test]
159 async fn policy_chain_defers_to_scripted_order() {
160 let script = ScriptedPolicy {
162 decisions: Mutex::new(VecDeque::from(vec![Decision::Ask])),
163 };
164 let chain = PolicyChain::new(vec![
165 Arc::new(script),
166 Arc::new(FixedPolicy(Decision::Allow)),
167 ]);
168 assert_eq!(
169 chain.evaluate(request_with_caps(vec![])).await.unwrap(),
170 Decision::Ask
171 );
172 }
173
174 async fn store_with_grant(capability: &Capability) -> Arc<SqliteStore> {
175 let path =
176 std::env::temp_dir().join(format!("kaynine-grant-test-{}.db", uuid::Uuid::new_v4()));
177 let store = SqliteStore::open(&path).expect("open store");
178 store.migrate().await.expect("migrate");
179 store
180 .insert_grant(
181 "grant-1",
182 "prod",
183 serde_json::json!({
184 "grant_id": "grant-1",
185 "namespace": "prod",
186 "capability": capability,
187 }),
188 )
189 .await
190 .expect("insert grant");
191 Arc::new(store)
194 }
195
196 #[tokio::test]
197 async fn grant_policy_allows_matched_capability() {
198 let cap = Capability::FileRead {
199 path: "/tmp/data".into(),
200 };
201 let store = store_with_grant(&cap).await;
202 let policy = GrantPolicy {
203 store: store as Arc<dyn SessionStore>,
204 namespace: "prod".into(),
205 };
206
207 assert_eq!(
209 policy
210 .evaluate(request_with_caps(vec![
211 Capability::NetworkAccess {
212 origin: "example.com".into()
213 },
214 cap.clone()
215 ]))
216 .await
217 .unwrap(),
218 Decision::Allow
219 );
220
221 assert_eq!(
223 policy
224 .evaluate(request_with_caps(vec![Capability::FileWrite {
225 path: "/tmp/data".into()
226 }]))
227 .await
228 .unwrap(),
229 Decision::Ask
230 );
231 }
232}