1use std::{
4 collections::{BTreeMap, HashMap, HashSet},
5 path::{Path, PathBuf},
6 sync::Mutex,
7};
8
9use cookie_agent_config::simple_wildcard_match;
10use cookie_agent_protocol::{
11 AgentSnapshot, ApprovalEvaluation, DecisionTrace, EffectivePermissionAction,
12 EffectivePermissionRule, EventPayload, MatchedPermissionRule, OperationFingerprint,
13 PermissionAction, PermissionEffect, PermissionRule, PermissionRuleSource,
14 PreparedOperationIdentity, SafeCode, SessionId, SessionOrigin, SessionPermissionGetResult,
15 SessionPermissionMutationResult, SessionPermissionOverlay, TreeApprovalGrant,
16 TreeApprovalGrantId, WildcardPattern,
17};
18use thiserror::Error;
19
20use crate::tool_api::UNSCOPED_PERMISSION_RESOURCE_DISPLAY;
21
22#[must_use]
24pub fn tool_visible(
25 rules: &[PermissionRule],
26 overlay: Option<&SessionPermissionOverlay>,
27 action: PermissionAction,
28) -> bool {
29 rules
30 .iter()
31 .chain(overlay.into_iter().flat_map(|overlay| &overlay.rules))
32 .any(|rule| {
33 rule.action == action
34 && matches!(rule.effect, PermissionEffect::Allow | PermissionEffect::Ask)
35 })
36}
37
38#[derive(Debug, Error)]
39pub enum PermissionError {
40 #[error("unknown permission name `{0}`")]
41 UnknownAction(String),
42}
43
44#[derive(Clone, Debug, Eq, Hash, PartialEq)]
45struct ApprovalKey {
46 root: SessionId,
47 fingerprint: OperationFingerprint,
48}
49
50#[derive(Debug, Default)]
51pub struct ApprovalStore {
52 grants: Mutex<HashMap<ApprovalKey, TreeApprovalGrant>>,
53}
54
55impl ApprovalStore {
56 pub fn replace(&self, grants: impl IntoIterator<Item = TreeApprovalGrant>) {
57 let mut stored = self
58 .grants
59 .lock()
60 .unwrap_or_else(|poisoned| poisoned.into_inner());
61 stored.clear();
62 for grant in grants {
63 stored.insert(
64 ApprovalKey {
65 root: grant.root_session_id,
66 fingerprint: grant.operation_fingerprint.clone(),
67 },
68 grant,
69 );
70 }
71 }
72
73 pub fn grant(&self, grant: TreeApprovalGrant) {
74 self.grants
75 .lock()
76 .unwrap_or_else(|poisoned| poisoned.into_inner())
77 .insert(
78 ApprovalKey {
79 root: grant.root_session_id,
80 fingerprint: grant.operation_fingerprint.clone(),
81 },
82 grant,
83 );
84 }
85
86 #[must_use]
87 pub fn matching(
88 &self,
89 root: SessionId,
90 operation: &PreparedOperationIdentity,
91 ) -> Option<TreeApprovalGrant> {
92 let fingerprint = OperationFingerprint::from_prepared_operation(operation);
93 self.grants
94 .lock()
95 .unwrap_or_else(|poisoned| poisoned.into_inner())
96 .get(&ApprovalKey { root, fingerprint })
97 .filter(|grant| {
98 grant.capabilities == operation.capabilities()
99 && grant.resources == operation.resources()
100 })
101 .cloned()
102 }
103
104 #[must_use]
105 pub fn for_root(&self, root: SessionId) -> Vec<TreeApprovalGrant> {
106 self.grants
107 .lock()
108 .unwrap_or_else(|poisoned| poisoned.into_inner())
109 .iter()
110 .filter(|(key, _)| key.root == root)
111 .map(|(_, grant)| grant.clone())
112 .collect()
113 }
114
115 pub fn invalidate_grants(&self, ids: &HashSet<TreeApprovalGrantId>) {
116 self.grants
117 .lock()
118 .unwrap_or_else(|poisoned| poisoned.into_inner())
119 .retain(|_, grant| !ids.contains(&grant.grant_id));
120 }
121}
122
123#[derive(Clone, Debug)]
124pub struct PermissionDecision {
125 pub effect: PermissionEffect,
126 pub evaluations: Vec<ApprovalEvaluation>,
127}
128
129#[derive(Debug, Default)]
130pub struct PermissionPipeline {
131 _private: (),
132}
133
134impl PermissionPipeline {
135 pub fn action_for_permission_name(
136 permission_name: &str,
137 ) -> Result<PermissionAction, PermissionError> {
138 match permission_name {
139 "read" => Ok(PermissionAction::Read),
140 "write" => Ok(PermissionAction::Write),
141 "bash" => Ok(PermissionAction::Bash),
142 "delegate" => Ok(PermissionAction::Delegate),
143 "message" => Ok(PermissionAction::Message),
144 "mcp" => Ok(PermissionAction::Mcp),
145 "plugin" => Ok(PermissionAction::Plugin),
146 "skill" => Ok(PermissionAction::Skill),
147 "webfetch" => Ok(PermissionAction::Webfetch),
148 other if other.starts_with("plugin:") && other.len() > "plugin:".len() => {
149 Ok(PermissionAction::Plugin)
150 }
151 other => Err(PermissionError::UnknownAction(other.into())),
152 }
153 }
154
155 #[must_use]
156 pub fn decide_operation(
157 &self,
158 policy: &AgentSnapshot,
159 operation: &PreparedOperationIdentity,
160 policy_labels: &[Option<String>],
161 workspace: &Path,
162 ) -> PermissionDecision {
163 self.decide_operation_with_overlay(policy, None, operation, policy_labels, workspace)
164 }
165
166 #[must_use]
167 pub fn decide_operation_with_overlay(
168 &self,
169 policy: &AgentSnapshot,
170 overlay: Option<&SessionPermissionOverlay>,
171 operation: &PreparedOperationIdentity,
172 policy_labels: &[Option<String>],
173 workspace: &Path,
174 ) -> PermissionDecision {
175 assert!(!operation.resources().is_empty());
176 assert_eq!(operation.resources().len(), policy_labels.len());
177 let evaluations = operation
178 .resources()
179 .iter()
180 .zip(policy_labels)
181 .map(|(resource, normalized)| {
182 let (candidates, effect, reason) = match normalized {
183 Some(normalized) => {
184 let candidates = matching_rules(
185 policy,
186 overlay,
187 resource.capability,
188 normalized,
189 workspace,
190 );
191 let (effect, reason) = effective_permission_with_overlay(
192 policy,
193 overlay,
194 resource.capability,
195 normalized,
196 workspace,
197 );
198 (candidates, effect, reason)
199 }
200 None => {
201 let candidates = matching_loose_rules(policy, overlay, resource.capability);
202 let (effect, reason) = effective_loose_permission_with_overlay(
203 policy,
204 overlay,
205 resource.capability,
206 );
207 (candidates, effect, reason)
208 }
209 };
210 ApprovalEvaluation {
211 resource_digest: resource.binding_digest.clone(),
212 effect,
213 trace: DecisionTrace {
214 action: resource.capability,
215 normalized_resource: normalized
216 .clone()
217 .unwrap_or_else(|| UNSCOPED_PERMISSION_RESOURCE_DISPLAY.to_owned()),
218 candidates,
219 effect,
220 precedence_reason: reason,
221 },
222 }
223 })
224 .collect::<Vec<_>>();
225 let effect = aggregate_effect(&evaluations);
226 PermissionDecision {
227 effect,
228 evaluations,
229 }
230 }
231
232 #[must_use]
233 pub fn decide_operation_with_grants(
234 &self,
235 policy: &AgentSnapshot,
236 overlay: Option<&SessionPermissionOverlay>,
237 grants: Option<&SessionPermissionOverlay>,
238 operation: &PreparedOperationIdentity,
239 policy_labels: &[Option<String>],
240 workspace: &Path,
241 ) -> PermissionDecision {
242 let mut decision = self.decide_operation_with_overlay(
243 policy,
244 overlay,
245 operation,
246 policy_labels,
247 workspace,
248 );
249 let Some(grants) = grants else {
250 return decision;
251 };
252 for (evaluation, (resource, normalized)) in decision
253 .evaluations
254 .iter_mut()
255 .zip(operation.resources().iter().zip(policy_labels))
256 {
257 if evaluation.effect != PermissionEffect::Ask {
258 continue;
259 }
260 let granted = grants.rules.iter().any(|rule| {
261 rule.effect == PermissionEffect::Allow
262 && rule.action == resource.capability
263 && normalized.as_ref().is_some_and(|normalized| {
264 permission_pattern_matches(
265 rule.resource.as_str(),
266 normalized,
267 &absolute_resource(workspace, normalized),
268 workspace,
269 )
270 })
271 });
272 if granted {
273 evaluation.effect = PermissionEffect::Allow;
274 evaluation.trace.effect = PermissionEffect::Allow;
275 evaluation.trace.precedence_reason =
276 "turn-scoped skill grant allows an otherwise ask operation".into();
277 }
278 }
279 decision.effect = aggregate_effect(&decision.evaluations);
280 decision
281 }
282
283 #[must_use]
284 pub fn tool_visible(policy: &AgentSnapshot, permission_name: &str) -> bool {
285 Self::tool_visible_with_overlay(policy, None, permission_name)
286 }
287
288 #[must_use]
289 pub fn tool_visible_with_overlay(
290 policy: &AgentSnapshot,
291 overlay: Option<&SessionPermissionOverlay>,
292 permission_name: &str,
293 ) -> bool {
294 let Ok(action) = Self::action_for_permission_name(permission_name) else {
295 return false;
296 };
297 tool_visible(&policy.permissions, overlay, action)
298 }
299
300 #[must_use]
301 pub fn tool_visible_with_grants(
302 policy: &AgentSnapshot,
303 overlay: Option<&SessionPermissionOverlay>,
304 grants: Option<&SessionPermissionOverlay>,
305 permission_name: &str,
306 workspace: &Path,
307 ) -> bool {
308 if Self::tool_visible_with_overlay(policy, overlay, permission_name) {
309 return true;
310 }
311 let Ok(action) = Self::action_for_permission_name(permission_name) else {
312 return false;
313 };
314 let plugin_permission = permission_name.strip_prefix("plugin:");
315 grants.is_some_and(|grants| {
316 grants.rules.iter().any(|grant| {
317 grant.action == action
318 && grant.effect == PermissionEffect::Allow
319 && plugin_permission.is_none_or(|permission| {
320 plugin_rule_governs(grant.resource.as_str(), permission)
321 })
322 && effective_permission_with_overlay(
323 policy,
324 overlay,
325 action,
326 grant.resource.as_str(),
327 workspace,
328 )
329 .0 != PermissionEffect::Deny
330 })
331 })
332 }
333}
334
335fn aggregate_effect(evaluations: &[ApprovalEvaluation]) -> PermissionEffect {
336 if evaluations
337 .iter()
338 .any(|evaluation| evaluation.effect == PermissionEffect::Deny)
339 {
340 PermissionEffect::Deny
341 } else if evaluations
342 .iter()
343 .any(|evaluation| evaluation.effect == PermissionEffect::Ask)
344 {
345 PermissionEffect::Ask
346 } else {
347 PermissionEffect::Allow
348 }
349}
350
351pub(crate) fn effective_loose_permission(
352 policy: &AgentSnapshot,
353 action: PermissionAction,
354) -> (PermissionEffect, String) {
355 policy
356 .permissions
357 .iter()
358 .enumerate()
359 .filter(|(_, rule)| rule.action == action && rule.resource.as_str() == "*")
360 .max_by_key(|(index, _)| *index)
361 .map_or_else(
362 || {
363 (
364 PermissionEffect::Deny,
365 "no bare or `*` rule; deny by default for a permission-name-only check".into(),
366 )
367 },
368 |(_, rule)| {
369 (
370 rule.effect,
371 "bare or `*` permission rule applies to the permission-name-only check".into(),
372 )
373 },
374 )
375}
376
377pub(crate) fn effective_loose_permission_with_overlay(
378 policy: &AgentSnapshot,
379 overlay: Option<&SessionPermissionOverlay>,
380 action: PermissionAction,
381) -> (PermissionEffect, String) {
382 overlay
383 .and_then(|overlay| {
384 overlay
385 .rules
386 .iter()
387 .enumerate()
388 .filter(|(_, rule)| rule.action == action && rule.resource.as_str() == "*")
389 .max_by_key(|(index, _)| *index)
390 })
391 .map_or_else(
392 || effective_loose_permission(policy, action),
393 |(_, rule)| {
394 (
395 rule.effect,
396 "session overlay wildcard rule takes precedence over the agent document".into(),
397 )
398 },
399 )
400}
401
402pub(crate) fn effective_permission(
403 policy: &AgentSnapshot,
404 action: PermissionAction,
405 resource: &str,
406 workspace: &Path,
407) -> (PermissionEffect, String) {
408 let absolute_resource = absolute_resource(workspace, resource);
409 let winner = policy
410 .permissions
411 .iter()
412 .enumerate()
413 .filter(|(_, rule)| {
414 rule.action == action
415 && permission_pattern_matches(
416 rule.resource.as_str(),
417 resource,
418 &absolute_resource,
419 workspace,
420 )
421 })
422 .max_by_key(|(index, rule)| specificity(rule.resource.as_str(), *index));
423 winner
424 .map_or_else(
425 || {
426 (
427 PermissionEffect::Deny,
428 "no matching rule; deny by default".into(),
429 )
430 },
431 |(_, rule)| {
432 (
433 rule.effect,
434 "most-specific matching pattern: more literal characters, then fewer wildcards, then later declaration".into(),
435 )
436 },
437 )
438}
439
440pub(crate) fn effective_permission_with_overlay(
441 policy: &AgentSnapshot,
442 overlay: Option<&SessionPermissionOverlay>,
443 action: PermissionAction,
444 resource: &str,
445 workspace: &Path,
446) -> (PermissionEffect, String) {
447 let absolute_resource = absolute_resource(workspace, resource);
448 let winner = overlay.and_then(|overlay| {
449 overlay
450 .rules
451 .iter()
452 .enumerate()
453 .filter(|(_, rule)| {
454 rule.action == action
455 && permission_pattern_matches(
456 rule.resource.as_str(),
457 resource,
458 &absolute_resource,
459 workspace,
460 )
461 })
462 .max_by_key(|(index, rule)| specificity(rule.resource.as_str(), *index))
463 });
464 winner.map_or_else(
465 || effective_permission(policy, action, resource, workspace),
466 |(_, rule)| {
467 (
468 rule.effect,
469 "session overlay matching rule takes precedence over the agent document".into(),
470 )
471 },
472 )
473}
474
475fn permission_pattern_matches(
476 pattern: &str,
477 relative_resource: &str,
478 absolute_resource: &str,
479 workspace: &Path,
480) -> bool {
481 if pattern.contains(cookie_agent_protocol::WildcardPattern::WORKSPACE_DIR_EXPRESSION) {
482 let expanded = expand_workspace_pattern(pattern, workspace);
483 simple_wildcard_match(&expanded, absolute_resource)
484 } else {
485 simple_wildcard_match(pattern, relative_resource)
486 }
487}
488
489fn plugin_rule_governs(pattern: &str, permission_name: &str) -> bool {
490 pattern == "*"
491 || simple_wildcard_match(pattern, permission_name)
492 || pattern
493 .strip_prefix(permission_name)
494 .is_some_and(|suffix| suffix.starts_with(' '))
495}
496
497fn expand_workspace_pattern(pattern: &str, workspace: &Path) -> String {
498 let workspace = normalized_path(&canonical_workspace(workspace));
499 if workspace == "/" {
500 pattern.replace("${workspace_dir}/", "/").replace(
501 cookie_agent_protocol::WildcardPattern::WORKSPACE_DIR_EXPRESSION,
502 "/",
503 )
504 } else {
505 pattern.replace(
506 cookie_agent_protocol::WildcardPattern::WORKSPACE_DIR_EXPRESSION,
507 workspace.trim_end_matches('/'),
508 )
509 }
510}
511
512fn absolute_resource(workspace: &Path, resource: &str) -> String {
513 if resource.starts_with("artifact://") {
514 return resource.to_owned();
515 }
516 let resource_path = Path::new(resource);
517 if resource_path.is_absolute() {
518 normalized_path(resource_path)
519 } else if resource_path == Path::new(".") {
520 normalized_path(&canonical_workspace(workspace))
521 } else {
522 normalized_path(&canonical_workspace(workspace).join(resource_path))
523 }
524}
525
526fn canonical_workspace(workspace: &Path) -> PathBuf {
527 workspace
528 .canonicalize()
529 .unwrap_or_else(|_| workspace.to_owned())
530}
531
532fn normalized_path(path: &Path) -> String {
533 path.to_string_lossy().replace('\\', "/")
534}
535
536fn specificity(
537 pattern: &str,
538 declaration_index: usize,
539) -> (usize, std::cmp::Reverse<usize>, usize) {
540 let wildcards = pattern
541 .chars()
542 .filter(|character| matches!(character, '*' | '?'))
543 .count();
544 let literals = pattern.chars().count() - wildcards;
545 (literals, std::cmp::Reverse(wildcards), declaration_index)
546}
547
548fn matching_rules(
549 policy: &AgentSnapshot,
550 overlay: Option<&SessionPermissionOverlay>,
551 action: PermissionAction,
552 resource: &str,
553 workspace: &Path,
554) -> Vec<MatchedPermissionRule> {
555 let absolute_resource = absolute_resource(workspace, resource);
556 policy
557 .permissions
558 .iter()
559 .filter(|rule| {
560 rule.action == action
561 && permission_pattern_matches(
562 rule.resource.as_str(),
563 resource,
564 &absolute_resource,
565 workspace,
566 )
567 })
568 .map(|rule| MatchedPermissionRule {
569 source_layer: SafeCode::new("agent_document").expect("static safe code"),
570 action: rule.action,
571 resource: rule.resource.clone(),
572 effect: rule.effect,
573 })
574 .chain(
575 overlay
576 .into_iter()
577 .flat_map(|overlay| overlay.rules.iter())
578 .filter(|rule| {
579 rule.action == action
580 && permission_pattern_matches(
581 rule.resource.as_str(),
582 resource,
583 &absolute_resource,
584 workspace,
585 )
586 })
587 .map(|rule| MatchedPermissionRule {
588 source_layer: SafeCode::new("session_overlay").expect("static safe code"),
589 action: rule.action,
590 resource: rule.resource.clone(),
591 effect: rule.effect,
592 }),
593 )
594 .collect()
595}
596
597fn matching_loose_rules(
598 policy: &AgentSnapshot,
599 overlay: Option<&SessionPermissionOverlay>,
600 action: PermissionAction,
601) -> Vec<MatchedPermissionRule> {
602 policy
603 .permissions
604 .iter()
605 .filter(|rule| rule.action == action && rule.resource.as_str() == "*")
606 .map(|rule| MatchedPermissionRule {
607 source_layer: SafeCode::new("agent_document").expect("static safe code"),
608 action: rule.action,
609 resource: rule.resource.clone(),
610 effect: rule.effect,
611 })
612 .chain(
613 overlay
614 .into_iter()
615 .flat_map(|overlay| overlay.rules.iter())
616 .filter(|rule| rule.action == action && rule.resource.as_str() == "*")
617 .map(|rule| MatchedPermissionRule {
618 source_layer: SafeCode::new("session_overlay").expect("static safe code"),
619 action: rule.action,
620 resource: rule.resource.clone(),
621 effect: rule.effect,
622 }),
623 )
624 .collect()
625}
626
627impl crate::Engine {
628 pub fn get_session_permissions(
629 &self,
630 session_id: SessionId,
631 ) -> Result<SessionPermissionGetResult, crate::EngineError> {
632 let session = self.inner.store.get(session_id)?;
633 let policy = governing_agent(&session);
634 Ok(SessionPermissionGetResult {
635 permissions: effective_permission_view(&policy, &session.permission_overlay),
636 current_mode: Some(self.permission_mode(session_id)),
637 })
638 }
639
640 pub async fn set_session_permission(
641 &self,
642 session_id: SessionId,
643 action: PermissionAction,
644 resource: WildcardPattern,
645 effect: PermissionEffect,
646 origin: cookie_agent_protocol::EventOrigin,
647 ) -> Result<SessionPermissionMutationResult, crate::EngineError> {
648 let _mutation = self
649 .inner
650 .approvals
651 .permission_overlay_mutation
652 .lock()
653 .await;
654 let session = self.inner.store.get(session_id)?;
655 let policy = governing_agent(&session);
656 let mut overlay = session.permission_overlay.clone();
657 let old_effect = rule_effect(
658 &policy,
659 &overlay,
660 action,
661 resource.as_str(),
662 self.inner.store.cwd(),
663 );
664 overlay
665 .rules
666 .retain(|rule| rule.action != action || rule.resource != resource);
667 overlay.rules.push(PermissionRule {
668 action,
669 resource: resource.clone(),
670 effect,
671 });
672 overlay
673 .validate()
674 .map_err(|error| crate::EngineError::Permission(error.to_string()))?;
675 let new_effect = rule_effect(
676 &policy,
677 &overlay,
678 action,
679 resource.as_str(),
680 self.inner.store.cwd(),
681 );
682 if effect_rank(new_effect) > effect_rank(old_effect) {
683 self.invalidate_action_grants(session_id, &session.meta.origin, action)?;
684 }
685 self.append(
686 session_id,
687 None,
688 origin,
689 EventPayload::SessionPermissionOverlaySet { overlay },
690 )
691 .await?;
692 let result = self.get_session_permissions(session_id)?;
693 Ok(SessionPermissionMutationResult {
694 permissions: result.permissions,
695 })
696 }
697
698 pub async fn clear_session_permission(
699 &self,
700 session_id: SessionId,
701 action: PermissionAction,
702 resource: &WildcardPattern,
703 origin: cookie_agent_protocol::EventOrigin,
704 ) -> Result<SessionPermissionMutationResult, crate::EngineError> {
705 let _mutation = self
706 .inner
707 .approvals
708 .permission_overlay_mutation
709 .lock()
710 .await;
711 let session = self.inner.store.get(session_id)?;
712 let policy = governing_agent(&session);
713 let mut overlay = session.permission_overlay;
714 let old_effect = rule_effect(
715 &policy,
716 &overlay,
717 action,
718 resource.as_str(),
719 self.inner.store.cwd(),
720 );
721 let prior_len = overlay.rules.len();
722 overlay
723 .rules
724 .retain(|rule| rule.action != action || &rule.resource != resource);
725 if overlay.rules.len() != prior_len {
726 let new_effect = rule_effect(
727 &policy,
728 &overlay,
729 action,
730 resource.as_str(),
731 self.inner.store.cwd(),
732 );
733 if effect_rank(new_effect) > effect_rank(old_effect) {
734 self.invalidate_action_grants(session_id, &session.meta.origin, action)?;
735 }
736 self.append(
737 session_id,
738 None,
739 origin,
740 EventPayload::SessionPermissionOverlaySet { overlay },
741 )
742 .await?;
743 }
744 let result = self.get_session_permissions(session_id)?;
745 Ok(SessionPermissionMutationResult {
746 permissions: result.permissions,
747 })
748 }
749
750 fn invalidate_action_grants(
751 &self,
752 session_id: SessionId,
753 origin: &SessionOrigin,
754 action: PermissionAction,
755 ) -> Result<(), crate::EngineError> {
756 let root = match origin {
757 SessionOrigin::Root => session_id,
758 SessionOrigin::Delegated {
759 root_session_id, ..
760 } => *root_session_id,
761 };
762 let grants = self
763 .inner
764 .approvals
765 .store
766 .for_root(root)
767 .into_iter()
768 .filter(|grant| {
769 grant
770 .resources
771 .iter()
772 .any(|resource| resource.capability == action)
773 })
774 .collect::<Vec<_>>();
775 let ids = grants
776 .iter()
777 .map(|grant| grant.grant_id)
778 .collect::<HashSet<_>>();
779 if ids.is_empty() {
780 return Ok(());
781 }
782 let digests = grants
783 .iter()
784 .flat_map(|grant| grant.resources.iter())
785 .filter(|resource| resource.capability == action)
786 .map(|resource| resource.binding_digest.digest().clone())
787 .collect();
788 self.inner
789 .grant_journal
790 .invalidate(root, ids.iter().copied().collect(), digests)?;
791 self.inner.approvals.store.invalidate_grants(&ids);
792 Ok(())
793 }
794}
795
796pub(crate) fn governing_agent_for_skills(
797 session: &crate::session::SessionProjection,
798) -> AgentSnapshot {
799 governing_agent(session)
800}
801
802fn governing_agent(session: &crate::session::SessionProjection) -> AgentSnapshot {
803 let latest_run =
804 session
805 .log
806 .event_snapshot()
807 .iter()
808 .rev()
809 .find_map(|event| match &event.payload {
810 EventPayload::RunStarted { agent, .. } => Some(agent.as_ref().clone()),
811 _ => None,
812 });
813 select_governing_agent(&session.creation_agent, latest_run.as_ref())
814}
815
816fn select_governing_agent(
817 creation_agent: &AgentSnapshot,
818 latest_run_agent: Option<&AgentSnapshot>,
819) -> AgentSnapshot {
820 latest_run_agent.unwrap_or(creation_agent).clone()
821}
822
823fn effect_rank(effect: PermissionEffect) -> u8 {
824 match effect {
825 PermissionEffect::Allow => 0,
826 PermissionEffect::Ask => 1,
827 PermissionEffect::Deny => 2,
828 }
829}
830
831fn rule_effect(
832 policy: &AgentSnapshot,
833 overlay: &SessionPermissionOverlay,
834 action: PermissionAction,
835 resource: &str,
836 workspace: &Path,
837) -> PermissionEffect {
838 if resource == "*" {
839 effective_loose_permission_with_overlay(policy, Some(overlay), action).0
840 } else {
841 effective_permission_with_overlay(policy, Some(overlay), action, resource, workspace).0
842 }
843}
844
845fn effective_permission_view(
846 policy: &AgentSnapshot,
847 overlay: &SessionPermissionOverlay,
848) -> Vec<EffectivePermissionAction> {
849 [
850 PermissionAction::Read,
851 PermissionAction::Write,
852 PermissionAction::Bash,
853 PermissionAction::Delegate,
854 PermissionAction::Message,
855 PermissionAction::Mcp,
856 PermissionAction::Plugin,
857 PermissionAction::Skill,
858 PermissionAction::Webfetch,
859 ]
860 .into_iter()
861 .map(|action| {
862 let overlay_wildcard = overlay
863 .rules
864 .iter()
865 .rev()
866 .find(|rule| rule.action == action && rule.resource.as_str() == "*");
867 let agent_wildcard = policy
868 .permissions
869 .iter()
870 .rev()
871 .find(|rule| rule.action == action && rule.resource.as_str() == "*");
872 let (effect, source) = overlay_wildcard.map_or_else(
873 || {
874 agent_wildcard.map_or(
875 (PermissionEffect::Deny, PermissionRuleSource::Default),
876 |rule| (rule.effect, PermissionRuleSource::AgentDocument),
877 )
878 },
879 |rule| (rule.effect, PermissionRuleSource::SessionOverlay),
880 );
881 let mut patterns = BTreeMap::new();
882 if overlay_wildcard.is_none() {
883 for rule in policy
884 .permissions
885 .iter()
886 .filter(|rule| rule.action == action && rule.resource.as_str() != "*")
887 {
888 patterns.insert(
889 rule.resource.as_str().to_owned(),
890 EffectivePermissionRule {
891 resource: rule.resource.clone(),
892 effect: rule.effect,
893 source: PermissionRuleSource::AgentDocument,
894 },
895 );
896 }
897 }
898 for rule in overlay
899 .rules
900 .iter()
901 .filter(|rule| rule.action == action && rule.resource.as_str() != "*")
902 {
903 patterns.insert(
904 rule.resource.as_str().to_owned(),
905 EffectivePermissionRule {
906 resource: rule.resource.clone(),
907 effect: rule.effect,
908 source: PermissionRuleSource::SessionOverlay,
909 },
910 );
911 }
912 EffectivePermissionAction {
913 action,
914 effect,
915 source,
916 patterns: patterns.into_values().collect(),
917 }
918 })
919 .collect()
920}
921
922#[cfg(test)]
923mod tests;