Skip to content

Commit 52e8d14

Browse files
authored
fix: resolve cross-package import errors breaking CI (#222)
Adds try/except ImportError fallbacks for circuit_breaker, adversarial, conflict_resolution, shadow, and marketplace shims. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 75d0b5c commit 52e8d14

File tree

10 files changed

+1617
-39
lines changed

10 files changed

+1617
-39
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""Standalone policy conflict resolution implementation.
4+
5+
This module provides a self-contained implementation that requires no
6+
packages beyond ``pydantic`` (already a core ``agentmesh`` dependency).
7+
It is used as a fallback by ``agentmesh.governance.conflict_resolution``
8+
when ``agent_os`` is not installed.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import logging
14+
from enum import Enum
15+
from typing import Any
16+
17+
from pydantic import BaseModel, Field
18+
19+
_logger = logging.getLogger(__name__)
20+
21+
22+
class ConflictResolutionStrategy(str, Enum):
23+
"""Strategy for resolving conflicts between competing policy decisions."""
24+
25+
DENY_OVERRIDES = "deny_overrides"
26+
ALLOW_OVERRIDES = "allow_overrides"
27+
PRIORITY_FIRST_MATCH = "priority_first_match"
28+
MOST_SPECIFIC_WINS = "most_specific_wins"
29+
30+
31+
class PolicyScope(str, Enum):
32+
"""Breadth of a policy's applicability.
33+
34+
Specificity order (most → least): AGENT > TENANT > GLOBAL.
35+
"""
36+
37+
GLOBAL = "global"
38+
TENANT = "tenant"
39+
AGENT = "agent"
40+
41+
42+
# Specificity rank: higher = more specific
43+
_SCOPE_SPECIFICITY: dict[Any, int] = {
44+
PolicyScope.GLOBAL: 0,
45+
PolicyScope.TENANT: 1,
46+
PolicyScope.AGENT: 2,
47+
}
48+
49+
50+
class CandidateDecision(BaseModel):
51+
"""A single policy decision candidate awaiting conflict resolution."""
52+
53+
action: str
54+
priority: int = 0
55+
scope: PolicyScope = PolicyScope.GLOBAL
56+
policy_name: str = ""
57+
rule_name: str = ""
58+
reason: str = ""
59+
approvers: list[str] = Field(default_factory=list)
60+
61+
@property
62+
def is_deny(self) -> bool:
63+
return self.action == "deny"
64+
65+
@property
66+
def is_allow(self) -> bool:
67+
return self.action == "allow"
68+
69+
@property
70+
def specificity(self) -> int:
71+
return _SCOPE_SPECIFICITY.get(self.scope, 0)
72+
73+
74+
class ResolutionResult(BaseModel):
75+
"""Outcome of conflict resolution."""
76+
77+
winning_decision: CandidateDecision
78+
strategy_used: ConflictResolutionStrategy
79+
candidates_evaluated: int = 0
80+
conflict_detected: bool = False
81+
resolution_trace: list[str] = Field(default_factory=list)
82+
83+
84+
class PolicyConflictResolver:
85+
"""Resolves conflicts between competing policy decisions."""
86+
87+
def __init__(
88+
self,
89+
strategy: ConflictResolutionStrategy = ConflictResolutionStrategy.PRIORITY_FIRST_MATCH,
90+
) -> None:
91+
self.strategy = strategy
92+
93+
def resolve(self, candidates: list[CandidateDecision]) -> ResolutionResult:
94+
"""Resolve a list of candidate decisions into a single winner."""
95+
if not candidates:
96+
raise ValueError("Cannot resolve conflict with zero candidates")
97+
if len(candidates) == 1:
98+
return ResolutionResult(
99+
winning_decision=candidates[0],
100+
strategy_used=self.strategy,
101+
candidates_evaluated=1,
102+
conflict_detected=False,
103+
resolution_trace=[
104+
f"Single candidate: {candidates[0].rule_name}{candidates[0].action}"
105+
],
106+
)
107+
actions = {c.action for c in candidates}
108+
conflict_detected = "allow" in actions and "deny" in actions
109+
dispatch = {
110+
ConflictResolutionStrategy.DENY_OVERRIDES: self._deny_overrides,
111+
ConflictResolutionStrategy.ALLOW_OVERRIDES: self._allow_overrides,
112+
ConflictResolutionStrategy.PRIORITY_FIRST_MATCH: self._priority_first_match,
113+
ConflictResolutionStrategy.MOST_SPECIFIC_WINS: self._most_specific_wins,
114+
}
115+
winner, trace = dispatch[self.strategy](candidates)
116+
return ResolutionResult(
117+
winning_decision=winner,
118+
strategy_used=self.strategy,
119+
candidates_evaluated=len(candidates),
120+
conflict_detected=conflict_detected,
121+
resolution_trace=trace,
122+
)
123+
124+
def _deny_overrides(
125+
self, candidates: list[CandidateDecision]
126+
) -> tuple[CandidateDecision, list[str]]:
127+
trace: list[str] = []
128+
denies = [c for c in candidates if c.is_deny]
129+
if denies:
130+
denies.sort(key=lambda c: c.priority, reverse=True)
131+
winner = denies[0]
132+
trace.append(f"DENY_OVERRIDES: {len(denies)} deny rule(s) found")
133+
trace.append(
134+
f"Winner: {winner.rule_name} "
135+
f"(priority={winner.priority}, scope={winner.scope.value})"
136+
)
137+
return winner, trace
138+
candidates_sorted = sorted(candidates, key=lambda c: c.priority, reverse=True)
139+
winner = candidates_sorted[0]
140+
trace.append("DENY_OVERRIDES: no deny rules, selecting highest-priority allow")
141+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority})")
142+
return winner, trace
143+
144+
def _allow_overrides(
145+
self, candidates: list[CandidateDecision]
146+
) -> tuple[CandidateDecision, list[str]]:
147+
trace: list[str] = []
148+
allows = [c for c in candidates if c.is_allow]
149+
if allows:
150+
allows.sort(key=lambda c: c.priority, reverse=True)
151+
winner = allows[0]
152+
trace.append(f"ALLOW_OVERRIDES: {len(allows)} allow rule(s) found")
153+
trace.append(
154+
f"Winner: {winner.rule_name} "
155+
f"(priority={winner.priority}, scope={winner.scope.value})"
156+
)
157+
return winner, trace
158+
candidates_sorted = sorted(candidates, key=lambda c: c.priority, reverse=True)
159+
winner = candidates_sorted[0]
160+
trace.append("ALLOW_OVERRIDES: no allow rules, selecting highest-priority deny")
161+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority})")
162+
return winner, trace
163+
164+
def _priority_first_match(
165+
self, candidates: list[CandidateDecision]
166+
) -> tuple[CandidateDecision, list[str]]:
167+
sorted_candidates = sorted(candidates, key=lambda c: c.priority, reverse=True)
168+
winner = sorted_candidates[0]
169+
trace = [
170+
f"PRIORITY_FIRST_MATCH: {len(candidates)} candidates",
171+
f"Winner: {winner.rule_name} (priority={winner.priority}, action={winner.action})",
172+
]
173+
return winner, trace
174+
175+
def _most_specific_wins(
176+
self, candidates: list[CandidateDecision]
177+
) -> tuple[CandidateDecision, list[str]]:
178+
sorted_candidates = sorted(
179+
candidates,
180+
key=lambda c: (c.specificity, c.priority),
181+
reverse=True,
182+
)
183+
winner = sorted_candidates[0]
184+
trace = [
185+
f"MOST_SPECIFIC_WINS: {len(candidates)} candidates",
186+
f"Specificity ranking: "
187+
f"{[(c.rule_name, c.scope.value, c.specificity) for c in sorted_candidates]}",
188+
f"Winner: {winner.rule_name} "
189+
f"(scope={winner.scope.value}, priority={winner.priority}, action={winner.action})",
190+
]
191+
return winner, trace
192+
193+
194+
__all__ = [
195+
"ConflictResolutionStrategy",
196+
"PolicyScope",
197+
"CandidateDecision",
198+
"ResolutionResult",
199+
"PolicyConflictResolver",
200+
]

0 commit comments

Comments
 (0)