agent_os_kernel 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_control_plane/__init__.py +662 -0
- agent_control_plane/a2a_adapter.py +543 -0
- agent_control_plane/adapter.py +417 -0
- agent_control_plane/agent_hibernation.py +394 -0
- agent_control_plane/agent_kernel.py +470 -0
- agent_control_plane/compliance.py +720 -0
- agent_control_plane/constraint_graphs.py +478 -0
- agent_control_plane/control_plane.py +854 -0
- agent_control_plane/example_executors.py +195 -0
- agent_control_plane/execution_engine.py +231 -0
- agent_control_plane/flight_recorder.py +846 -0
- agent_control_plane/governance_layer.py +435 -0
- agent_control_plane/hf_utils.py +563 -0
- agent_control_plane/interfaces/__init__.py +55 -0
- agent_control_plane/interfaces/kernel_interface.py +361 -0
- agent_control_plane/interfaces/plugin_interface.py +497 -0
- agent_control_plane/interfaces/protocol_interfaces.py +387 -0
- agent_control_plane/kernel_space.py +1009 -0
- agent_control_plane/langchain_adapter.py +424 -0
- agent_control_plane/lifecycle.py +3113 -0
- agent_control_plane/mcp_adapter.py +653 -0
- agent_control_plane/ml_safety.py +563 -0
- agent_control_plane/multimodal.py +727 -0
- agent_control_plane/mute_agent.py +422 -0
- agent_control_plane/observability.py +787 -0
- agent_control_plane/orchestrator.py +482 -0
- agent_control_plane/plugin_registry.py +750 -0
- agent_control_plane/policy_engine.py +954 -0
- agent_control_plane/process_isolation.py +777 -0
- agent_control_plane/shadow_mode.py +310 -0
- agent_control_plane/signals.py +493 -0
- agent_control_plane/supervisor_agents.py +430 -0
- agent_control_plane/time_travel_debugger.py +557 -0
- agent_control_plane/tool_registry.py +452 -0
- agent_control_plane/vfs.py +697 -0
- agent_kernel/__init__.py +69 -0
- agent_kernel/analyzer.py +435 -0
- agent_kernel/auditor.py +36 -0
- agent_kernel/completeness_auditor.py +237 -0
- agent_kernel/detector.py +203 -0
- agent_kernel/kernel.py +744 -0
- agent_kernel/memory_manager.py +85 -0
- agent_kernel/models.py +374 -0
- agent_kernel/nudge_mechanism.py +263 -0
- agent_kernel/outcome_analyzer.py +338 -0
- agent_kernel/patcher.py +582 -0
- agent_kernel/semantic_analyzer.py +316 -0
- agent_kernel/semantic_purge.py +349 -0
- agent_kernel/simulator.py +449 -0
- agent_kernel/teacher.py +85 -0
- agent_kernel/triage.py +152 -0
- agent_os/__init__.py +409 -0
- agent_os/_adversarial_impl.py +200 -0
- agent_os/_circuit_breaker_impl.py +232 -0
- agent_os/_mcp_metrics.py +193 -0
- agent_os/adversarial.py +20 -0
- agent_os/agents_compat.py +490 -0
- agent_os/audit_logger.py +135 -0
- agent_os/base_agent.py +651 -0
- agent_os/circuit_breaker.py +34 -0
- agent_os/cli/__init__.py +659 -0
- agent_os/cli/cmd_audit.py +128 -0
- agent_os/cli/cmd_init.py +152 -0
- agent_os/cli/cmd_policy.py +41 -0
- agent_os/cli/cmd_policy_gen.py +180 -0
- agent_os/cli/cmd_validate.py +258 -0
- agent_os/cli/mcp_scan.py +265 -0
- agent_os/cli/output.py +192 -0
- agent_os/cli/policy_checker.py +330 -0
- agent_os/compat.py +74 -0
- agent_os/constraint_graph.py +234 -0
- agent_os/content_governance.py +140 -0
- agent_os/context_budget.py +305 -0
- agent_os/credential_redactor.py +224 -0
- agent_os/diff_policy.py +89 -0
- agent_os/egress_policy.py +159 -0
- agent_os/escalation.py +276 -0
- agent_os/event_bus.py +124 -0
- agent_os/exceptions.py +180 -0
- agent_os/execution_context_policy.py +141 -0
- agent_os/github_enterprise.py +96 -0
- agent_os/health.py +20 -0
- agent_os/integrations/__init__.py +279 -0
- agent_os/integrations/a2a_adapter.py +279 -0
- agent_os/integrations/agent_lightning/__init__.py +30 -0
- agent_os/integrations/anthropic_adapter.py +420 -0
- agent_os/integrations/autogen_adapter.py +620 -0
- agent_os/integrations/base.py +1137 -0
- agent_os/integrations/compat.py +229 -0
- agent_os/integrations/config.py +98 -0
- agent_os/integrations/conversation_guardian.py +957 -0
- agent_os/integrations/crewai_adapter.py +467 -0
- agent_os/integrations/drift_detector.py +425 -0
- agent_os/integrations/dry_run.py +124 -0
- agent_os/integrations/escalation.py +582 -0
- agent_os/integrations/gemini_adapter.py +364 -0
- agent_os/integrations/google_adk_adapter.py +633 -0
- agent_os/integrations/guardrails_adapter.py +394 -0
- agent_os/integrations/health.py +197 -0
- agent_os/integrations/langchain_adapter.py +654 -0
- agent_os/integrations/llamafirewall.py +343 -0
- agent_os/integrations/llamaindex_adapter.py +188 -0
- agent_os/integrations/logging.py +191 -0
- agent_os/integrations/maf_adapter.py +631 -0
- agent_os/integrations/mistral_adapter.py +365 -0
- agent_os/integrations/openai_adapter.py +816 -0
- agent_os/integrations/openai_agents_sdk.py +406 -0
- agent_os/integrations/policy_compose.py +171 -0
- agent_os/integrations/profiling.py +144 -0
- agent_os/integrations/pydantic_ai_adapter.py +420 -0
- agent_os/integrations/rate_limiter.py +130 -0
- agent_os/integrations/rbac.py +143 -0
- agent_os/integrations/registry.py +113 -0
- agent_os/integrations/scope_guard.py +303 -0
- agent_os/integrations/semantic_kernel_adapter.py +769 -0
- agent_os/integrations/smolagents_adapter.py +629 -0
- agent_os/integrations/templates.py +178 -0
- agent_os/integrations/token_budget.py +134 -0
- agent_os/integrations/tool_aliases.py +190 -0
- agent_os/integrations/webhooks.py +177 -0
- agent_os/lite.py +208 -0
- agent_os/mcp_gateway.py +385 -0
- agent_os/mcp_message_signer.py +273 -0
- agent_os/mcp_protocols.py +161 -0
- agent_os/mcp_response_scanner.py +232 -0
- agent_os/mcp_security.py +924 -0
- agent_os/mcp_session_auth.py +231 -0
- agent_os/mcp_sliding_rate_limiter.py +184 -0
- agent_os/memory_guard.py +409 -0
- agent_os/metrics.py +134 -0
- agent_os/mute.py +428 -0
- agent_os/mute_agent.py +209 -0
- agent_os/policies/__init__.py +77 -0
- agent_os/policies/async_evaluator.py +275 -0
- agent_os/policies/backends.py +670 -0
- agent_os/policies/bridge.py +169 -0
- agent_os/policies/budget.py +85 -0
- agent_os/policies/cli.py +294 -0
- agent_os/policies/conflict_resolution.py +270 -0
- agent_os/policies/data_classification.py +252 -0
- agent_os/policies/evaluator.py +239 -0
- agent_os/policies/policy_schema.json +228 -0
- agent_os/policies/rate_limiting.py +145 -0
- agent_os/policies/schema.py +115 -0
- agent_os/policies/shared.py +331 -0
- agent_os/prompt_injection.py +694 -0
- agent_os/providers.py +182 -0
- agent_os/py.typed +0 -0
- agent_os/retry.py +81 -0
- agent_os/reversibility.py +251 -0
- agent_os/sandbox.py +432 -0
- agent_os/sandbox_provider.py +140 -0
- agent_os/secure_codegen.py +525 -0
- agent_os/security_skills.py +538 -0
- agent_os/semantic_policy.py +422 -0
- agent_os/server/__init__.py +15 -0
- agent_os/server/__main__.py +25 -0
- agent_os/server/app.py +277 -0
- agent_os/server/models.py +104 -0
- agent_os/shift_left_metrics.py +130 -0
- agent_os/stateless.py +742 -0
- agent_os/supervisor.py +148 -0
- agent_os/task_outcome.py +148 -0
- agent_os/transparency.py +181 -0
- agent_os/trust_root.py +128 -0
- agent_os_kernel-3.1.0.dist-info/METADATA +1269 -0
- agent_os_kernel-3.1.0.dist-info/RECORD +337 -0
- agent_os_kernel-3.1.0.dist-info/WHEEL +4 -0
- agent_os_kernel-3.1.0.dist-info/entry_points.txt +2 -0
- agent_os_kernel-3.1.0.dist-info/licenses/LICENSE +21 -0
- agent_os_observability/__init__.py +27 -0
- agent_os_observability/dashboards.py +898 -0
- agent_os_observability/metrics.py +398 -0
- agent_os_observability/server.py +223 -0
- agent_os_observability/tracer.py +232 -0
- agent_primitives/__init__.py +24 -0
- agent_primitives/failures.py +84 -0
- agent_primitives/py.typed +0 -0
- amb_core/__init__.py +177 -0
- amb_core/adapters/__init__.py +57 -0
- amb_core/adapters/aws_sqs_broker.py +376 -0
- amb_core/adapters/azure_servicebus_broker.py +340 -0
- amb_core/adapters/kafka_broker.py +260 -0
- amb_core/adapters/nats_broker.py +285 -0
- amb_core/adapters/rabbitmq_broker.py +235 -0
- amb_core/adapters/redis_broker.py +262 -0
- amb_core/broker.py +145 -0
- amb_core/bus.py +481 -0
- amb_core/cloudevents.py +509 -0
- amb_core/dlq.py +345 -0
- amb_core/hf_utils.py +536 -0
- amb_core/memory_broker.py +410 -0
- amb_core/models.py +141 -0
- amb_core/persistence.py +529 -0
- amb_core/schema.py +294 -0
- amb_core/tracing.py +358 -0
- atr/__init__.py +640 -0
- atr/access.py +348 -0
- atr/composition.py +645 -0
- atr/decorator.py +357 -0
- atr/executor.py +384 -0
- atr/health.py +557 -0
- atr/hf_utils.py +449 -0
- atr/injection.py +422 -0
- atr/metrics.py +440 -0
- atr/policies.py +403 -0
- atr/py.typed +2 -0
- atr/registry.py +452 -0
- atr/schema.py +480 -0
- atr/tools/safe/__init__.py +75 -0
- atr/tools/safe/calculator.py +467 -0
- atr/tools/safe/datetime_tool.py +443 -0
- atr/tools/safe/file_reader.py +402 -0
- atr/tools/safe/http_client.py +316 -0
- atr/tools/safe/json_parser.py +374 -0
- atr/tools/safe/text_tool.py +537 -0
- atr/tools/safe/toolkit.py +175 -0
- caas/__init__.py +162 -0
- caas/api/__init__.py +7 -0
- caas/api/server.py +1328 -0
- caas/caching.py +834 -0
- caas/cli.py +210 -0
- caas/conversation.py +223 -0
- caas/decay.py +72 -0
- caas/detection/__init__.py +9 -0
- caas/detection/detector.py +238 -0
- caas/enrichment.py +130 -0
- caas/gateway/__init__.py +27 -0
- caas/gateway/trust_gateway.py +474 -0
- caas/hf_utils.py +479 -0
- caas/ingestion/__init__.py +23 -0
- caas/ingestion/processors.py +253 -0
- caas/ingestion/structure_parser.py +188 -0
- caas/models.py +356 -0
- caas/pragmatic_truth.py +444 -0
- caas/routing/__init__.py +10 -0
- caas/routing/heuristic_router.py +58 -0
- caas/storage/__init__.py +9 -0
- caas/storage/store.py +389 -0
- caas/triad.py +213 -0
- caas/tuning/__init__.py +9 -0
- caas/tuning/tuner.py +329 -0
- caas/vfs/__init__.py +14 -0
- caas/vfs/filesystem.py +452 -0
- cmvk/__init__.py +218 -0
- cmvk/audit.py +402 -0
- cmvk/benchmarks.py +478 -0
- cmvk/constitutional.py +904 -0
- cmvk/hf_utils.py +301 -0
- cmvk/metrics.py +473 -0
- cmvk/profiles.py +300 -0
- cmvk/py.typed +0 -0
- cmvk/types.py +12 -0
- cmvk/verification.py +956 -0
- emk/__init__.py +89 -0
- emk/causal.py +352 -0
- emk/hf_utils.py +421 -0
- emk/indexer.py +83 -0
- emk/py.typed +0 -0
- emk/schema.py +204 -0
- emk/sleep_cycle.py +347 -0
- emk/store.py +281 -0
- iatp/__init__.py +166 -0
- iatp/attestation.py +461 -0
- iatp/cli.py +317 -0
- iatp/hf_utils.py +472 -0
- iatp/ipc_pipes.py +580 -0
- iatp/main.py +412 -0
- iatp/models/__init__.py +447 -0
- iatp/policy_engine.py +337 -0
- iatp/py.typed +2 -0
- iatp/recovery.py +321 -0
- iatp/security/__init__.py +270 -0
- iatp/sidecar/__init__.py +519 -0
- iatp/telemetry/__init__.py +164 -0
- iatp/tests/__init__.py +1 -0
- iatp/tests/test_attestation.py +370 -0
- iatp/tests/test_cli.py +131 -0
- iatp/tests/test_ed25519_attestation.py +211 -0
- iatp/tests/test_models.py +130 -0
- iatp/tests/test_policy_engine.py +347 -0
- iatp/tests/test_recovery.py +281 -0
- iatp/tests/test_security.py +222 -0
- iatp/tests/test_sidecar.py +167 -0
- iatp/tests/test_telemetry.py +175 -0
- mcp_kernel_server/__init__.py +28 -0
- mcp_kernel_server/cli.py +274 -0
- mcp_kernel_server/resources.py +217 -0
- mcp_kernel_server/server.py +564 -0
- mcp_kernel_server/tools.py +1174 -0
- mute_agent/__init__.py +68 -0
- mute_agent/core/__init__.py +1 -0
- mute_agent/core/execution_agent.py +166 -0
- mute_agent/core/handshake_protocol.py +201 -0
- mute_agent/core/reasoning_agent.py +238 -0
- mute_agent/knowledge_graph/__init__.py +1 -0
- mute_agent/knowledge_graph/graph_elements.py +65 -0
- mute_agent/knowledge_graph/multidimensional_graph.py +170 -0
- mute_agent/knowledge_graph/subgraph.py +224 -0
- mute_agent/listener/__init__.py +43 -0
- mute_agent/listener/adapters/__init__.py +31 -0
- mute_agent/listener/adapters/base_adapter.py +189 -0
- mute_agent/listener/adapters/caas_adapter.py +344 -0
- mute_agent/listener/adapters/control_plane_adapter.py +436 -0
- mute_agent/listener/adapters/iatp_adapter.py +332 -0
- mute_agent/listener/adapters/scak_adapter.py +251 -0
- mute_agent/listener/listener.py +610 -0
- mute_agent/listener/state_observer.py +436 -0
- mute_agent/listener/threshold_config.py +313 -0
- mute_agent/super_system/__init__.py +1 -0
- mute_agent/super_system/router.py +204 -0
- mute_agent/visualization/__init__.py +10 -0
- mute_agent/visualization/graph_debugger.py +502 -0
- nexus/README.md +60 -0
- nexus/__init__.py +51 -0
- nexus/arbiter.py +359 -0
- nexus/client.py +466 -0
- nexus/dmz.py +444 -0
- nexus/escrow.py +430 -0
- nexus/exceptions.py +286 -0
- nexus/pyproject.toml +36 -0
- nexus/registry.py +393 -0
- nexus/reputation.py +425 -0
- nexus/schemas/__init__.py +51 -0
- nexus/schemas/compliance.py +276 -0
- nexus/schemas/escrow.py +251 -0
- nexus/schemas/manifest.py +225 -0
- nexus/schemas/receipt.py +208 -0
- nexus/tests/__init__.py +0 -0
- nexus/tests/conftest.py +146 -0
- nexus/tests/test_arbiter.py +192 -0
- nexus/tests/test_dmz.py +194 -0
- nexus/tests/test_escrow.py +276 -0
- nexus/tests/test_exceptions.py +225 -0
- nexus/tests/test_registry.py +232 -0
- nexus/tests/test_reputation.py +328 -0
- nexus/tests/test_schemas.py +295 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""
|
|
4
|
+
Policy Conflict Resolution.
|
|
5
|
+
|
|
6
|
+
When multiple policies apply to the same agent action, the conflict
|
|
7
|
+
resolution strategy determines which decision wins.
|
|
8
|
+
|
|
9
|
+
Strategies
|
|
10
|
+
----------
|
|
11
|
+
- **DENY_OVERRIDES** (safest): If ANY matching rule denies, the action
|
|
12
|
+
is denied regardless of what other rules say. Standard in XACML and
|
|
13
|
+
most enterprise policy systems.
|
|
14
|
+
- **ALLOW_OVERRIDES**: If ANY matching rule allows, the action is
|
|
15
|
+
allowed. Useful for exception-based governance where you want
|
|
16
|
+
explicit allow-rules to punch through default-deny policies.
|
|
17
|
+
- **PRIORITY_FIRST_MATCH** (current default): Rules are sorted by
|
|
18
|
+
priority (highest first), and the first matching rule wins. This
|
|
19
|
+
preserves backward compatibility with the existing PolicyEngine.
|
|
20
|
+
- **MOST_SPECIFIC_WINS**: Agent-scoped rules override tenant-scoped,
|
|
21
|
+
which override global-scoped. Within the same scope, priority breaks
|
|
22
|
+
ties. Models the intuition that "closer policies override distant ones."
|
|
23
|
+
|
|
24
|
+
Scopes
|
|
25
|
+
------
|
|
26
|
+
Each ``Policy`` can declare a ``scope`` that indicates its breadth:
|
|
27
|
+
|
|
28
|
+
- ``global``: Organization-wide default policies
|
|
29
|
+
- ``tenant``: Applied to a specific tenant or team
|
|
30
|
+
- ``agent``: Applied to a specific agent instance
|
|
31
|
+
|
|
32
|
+
When ``MOST_SPECIFIC_WINS`` is active, scope determines precedence.
|
|
33
|
+
When other strategies are active, scope is informational metadata.
|
|
34
|
+
|
|
35
|
+
Usage::
|
|
36
|
+
|
|
37
|
+
from agentmesh.governance.conflict_resolution import (
|
|
38
|
+
ConflictResolutionStrategy,
|
|
39
|
+
PolicyScope,
|
|
40
|
+
PolicyConflictResolver,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
resolver = PolicyConflictResolver(ConflictResolutionStrategy.DENY_OVERRIDES)
|
|
44
|
+
final = resolver.resolve(candidate_decisions)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import logging
|
|
50
|
+
from enum import Enum
|
|
51
|
+
|
|
52
|
+
from pydantic import BaseModel, Field
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConflictResolutionStrategy(str, Enum):
|
|
58
|
+
"""Strategy for resolving conflicts between competing policy decisions."""
|
|
59
|
+
|
|
60
|
+
DENY_OVERRIDES = "deny_overrides"
|
|
61
|
+
ALLOW_OVERRIDES = "allow_overrides"
|
|
62
|
+
PRIORITY_FIRST_MATCH = "priority_first_match"
|
|
63
|
+
MOST_SPECIFIC_WINS = "most_specific_wins"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PolicyScope(str, Enum):
|
|
67
|
+
"""Breadth of a policy's applicability.
|
|
68
|
+
|
|
69
|
+
Specificity order (most → least): AGENT > ORGANIZATION > TENANT > GLOBAL.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
GLOBAL = "global"
|
|
73
|
+
TENANT = "tenant"
|
|
74
|
+
ORGANIZATION = "organization"
|
|
75
|
+
AGENT = "agent"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Specificity rank: higher = more specific
|
|
79
|
+
_SCOPE_SPECIFICITY: dict[PolicyScope, int] = {
|
|
80
|
+
PolicyScope.GLOBAL: 0,
|
|
81
|
+
PolicyScope.TENANT: 1,
|
|
82
|
+
PolicyScope.ORGANIZATION: 2,
|
|
83
|
+
PolicyScope.AGENT: 3,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CandidateDecision(BaseModel):
|
|
88
|
+
"""A single policy decision candidate awaiting conflict resolution.
|
|
89
|
+
|
|
90
|
+
Groups a decision with its originating policy metadata so the
|
|
91
|
+
resolver can apply scope- and priority-aware strategies.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
action: The action the rule dictates (allow, deny, warn, etc.).
|
|
95
|
+
priority: Numeric priority from the matched rule.
|
|
96
|
+
scope: Scope of the policy that produced this decision.
|
|
97
|
+
policy_name: Name of the originating policy.
|
|
98
|
+
rule_name: Name of the matched rule.
|
|
99
|
+
reason: Human-readable explanation.
|
|
100
|
+
approvers: Required approvers for ``require_approval`` actions.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
action: str
|
|
104
|
+
priority: int = 0
|
|
105
|
+
scope: PolicyScope = PolicyScope.GLOBAL
|
|
106
|
+
policy_name: str = ""
|
|
107
|
+
rule_name: str = ""
|
|
108
|
+
reason: str = ""
|
|
109
|
+
approvers: list[str] = Field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_deny(self) -> bool:
|
|
113
|
+
return self.action == "deny"
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_allow(self) -> bool:
|
|
117
|
+
return self.action == "allow"
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def specificity(self) -> int:
|
|
121
|
+
return _SCOPE_SPECIFICITY.get(self.scope, 0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ResolutionResult(BaseModel):
|
|
125
|
+
"""Outcome of conflict resolution.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
winning_decision: The decision that prevailed.
|
|
129
|
+
strategy_used: Which strategy resolved the conflict.
|
|
130
|
+
candidates_evaluated: How many candidates were considered.
|
|
131
|
+
conflict_detected: Whether genuinely conflicting decisions existed.
|
|
132
|
+
resolution_trace: Human-readable trace of the resolution logic.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
winning_decision: CandidateDecision
|
|
136
|
+
strategy_used: ConflictResolutionStrategy
|
|
137
|
+
candidates_evaluated: int = 0
|
|
138
|
+
conflict_detected: bool = False
|
|
139
|
+
resolution_trace: list[str] = Field(default_factory=list)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class PolicyConflictResolver:
|
|
143
|
+
"""Resolves conflicts between competing policy decisions.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
strategy: The conflict resolution strategy to apply.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
strategy: ConflictResolutionStrategy = ConflictResolutionStrategy.PRIORITY_FIRST_MATCH,
|
|
152
|
+
) -> None:
|
|
153
|
+
self.strategy = strategy
|
|
154
|
+
|
|
155
|
+
def resolve(self, candidates: list[CandidateDecision]) -> ResolutionResult:
|
|
156
|
+
"""Resolve a list of candidate decisions into a single winner.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
candidates: One or more candidate decisions from matching rules.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
A ``ResolutionResult`` containing the winning decision and
|
|
163
|
+
a trace of the resolution logic.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If ``candidates`` is empty.
|
|
167
|
+
"""
|
|
168
|
+
if not candidates:
|
|
169
|
+
raise ValueError("Cannot resolve conflict with zero candidates")
|
|
170
|
+
|
|
171
|
+
if len(candidates) == 1:
|
|
172
|
+
return ResolutionResult(
|
|
173
|
+
winning_decision=candidates[0],
|
|
174
|
+
strategy_used=self.strategy,
|
|
175
|
+
candidates_evaluated=1,
|
|
176
|
+
conflict_detected=False,
|
|
177
|
+
resolution_trace=[f"Single candidate: {candidates[0].rule_name} → {candidates[0].action}"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Detect genuine conflict (mix of allow and deny)
|
|
181
|
+
actions = {c.action for c in candidates}
|
|
182
|
+
conflict_detected = "allow" in actions and "deny" in actions
|
|
183
|
+
|
|
184
|
+
dispatch = {
|
|
185
|
+
ConflictResolutionStrategy.DENY_OVERRIDES: self._deny_overrides,
|
|
186
|
+
ConflictResolutionStrategy.ALLOW_OVERRIDES: self._allow_overrides,
|
|
187
|
+
ConflictResolutionStrategy.PRIORITY_FIRST_MATCH: self._priority_first_match,
|
|
188
|
+
ConflictResolutionStrategy.MOST_SPECIFIC_WINS: self._most_specific_wins,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
winner, trace = dispatch[self.strategy](candidates)
|
|
192
|
+
|
|
193
|
+
return ResolutionResult(
|
|
194
|
+
winning_decision=winner,
|
|
195
|
+
strategy_used=self.strategy,
|
|
196
|
+
candidates_evaluated=len(candidates),
|
|
197
|
+
conflict_detected=conflict_detected,
|
|
198
|
+
resolution_trace=trace,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# ── Strategy implementations ────────────────────────────
|
|
202
|
+
|
|
203
|
+
def _deny_overrides(
|
|
204
|
+
self, candidates: list[CandidateDecision]
|
|
205
|
+
) -> tuple[CandidateDecision, list[str]]:
|
|
206
|
+
"""DENY_OVERRIDES: any deny wins. Among denies, highest priority wins."""
|
|
207
|
+
trace = []
|
|
208
|
+
denies = [c for c in candidates if c.is_deny]
|
|
209
|
+
if denies:
|
|
210
|
+
denies.sort(key=lambda c: c.priority, reverse=True)
|
|
211
|
+
winner = denies[0]
|
|
212
|
+
trace.append(f"DENY_OVERRIDES: {len(denies)} deny rule(s) found")
|
|
213
|
+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority}, scope={winner.scope.value})")
|
|
214
|
+
return winner, trace
|
|
215
|
+
|
|
216
|
+
# No denies — pick highest-priority allow
|
|
217
|
+
candidates_sorted = sorted(candidates, key=lambda c: c.priority, reverse=True)
|
|
218
|
+
winner = candidates_sorted[0]
|
|
219
|
+
trace.append("DENY_OVERRIDES: no deny rules, selecting highest-priority allow")
|
|
220
|
+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority})")
|
|
221
|
+
return winner, trace
|
|
222
|
+
|
|
223
|
+
def _allow_overrides(
|
|
224
|
+
self, candidates: list[CandidateDecision]
|
|
225
|
+
) -> tuple[CandidateDecision, list[str]]:
|
|
226
|
+
"""ALLOW_OVERRIDES: any allow wins. Among allows, highest priority wins."""
|
|
227
|
+
trace = []
|
|
228
|
+
allows = [c for c in candidates if c.is_allow]
|
|
229
|
+
if allows:
|
|
230
|
+
allows.sort(key=lambda c: c.priority, reverse=True)
|
|
231
|
+
winner = allows[0]
|
|
232
|
+
trace.append(f"ALLOW_OVERRIDES: {len(allows)} allow rule(s) found")
|
|
233
|
+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority}, scope={winner.scope.value})")
|
|
234
|
+
return winner, trace
|
|
235
|
+
|
|
236
|
+
# No allows — pick highest-priority deny
|
|
237
|
+
candidates_sorted = sorted(candidates, key=lambda c: c.priority, reverse=True)
|
|
238
|
+
winner = candidates_sorted[0]
|
|
239
|
+
trace.append("ALLOW_OVERRIDES: no allow rules, selecting highest-priority deny")
|
|
240
|
+
trace.append(f"Winner: {winner.rule_name} (priority={winner.priority})")
|
|
241
|
+
return winner, trace
|
|
242
|
+
|
|
243
|
+
def _priority_first_match(
|
|
244
|
+
self, candidates: list[CandidateDecision]
|
|
245
|
+
) -> tuple[CandidateDecision, list[str]]:
|
|
246
|
+
"""PRIORITY_FIRST_MATCH: highest priority wins regardless of action."""
|
|
247
|
+
sorted_candidates = sorted(candidates, key=lambda c: c.priority, reverse=True)
|
|
248
|
+
winner = sorted_candidates[0]
|
|
249
|
+
trace = [
|
|
250
|
+
f"PRIORITY_FIRST_MATCH: {len(candidates)} candidates",
|
|
251
|
+
f"Winner: {winner.rule_name} (priority={winner.priority}, action={winner.action})",
|
|
252
|
+
]
|
|
253
|
+
return winner, trace
|
|
254
|
+
|
|
255
|
+
def _most_specific_wins(
|
|
256
|
+
self, candidates: list[CandidateDecision]
|
|
257
|
+
) -> tuple[CandidateDecision, list[str]]:
|
|
258
|
+
"""MOST_SPECIFIC_WINS: agent > tenant > global. Priority breaks ties."""
|
|
259
|
+
sorted_candidates = sorted(
|
|
260
|
+
candidates,
|
|
261
|
+
key=lambda c: (c.specificity, c.priority),
|
|
262
|
+
reverse=True,
|
|
263
|
+
)
|
|
264
|
+
winner = sorted_candidates[0]
|
|
265
|
+
trace = [
|
|
266
|
+
f"MOST_SPECIFIC_WINS: {len(candidates)} candidates",
|
|
267
|
+
f"Specificity ranking: {[(c.rule_name, c.scope.value, c.specificity) for c in sorted_candidates]}",
|
|
268
|
+
f"Winner: {winner.rule_name} (scope={winner.scope.value}, priority={winner.priority}, action={winner.action})",
|
|
269
|
+
]
|
|
270
|
+
return winner, trace
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Data-layer ABAC policies for AI agent governance.
|
|
4
|
+
|
|
5
|
+
Extends the policy engine beyond tool-level to data-level enforcement.
|
|
6
|
+
Agents are checked not just for which tools they can use, but which
|
|
7
|
+
data classifications they can access.
|
|
8
|
+
|
|
9
|
+
Addresses the market gap where 63% of organizations cannot stop
|
|
10
|
+
their AI from accessing unauthorized data (Kiteworks 2026 research).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import IntEnum
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DataClassification(IntEnum):
|
|
24
|
+
"""Sensitivity levels for data classification, ordered by sensitivity."""
|
|
25
|
+
|
|
26
|
+
PUBLIC = 0
|
|
27
|
+
INTERNAL = 1
|
|
28
|
+
CONFIDENTIAL = 2
|
|
29
|
+
RESTRICTED = 3
|
|
30
|
+
TOP_SECRET = 4
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DataLabel(BaseModel):
|
|
34
|
+
"""Label describing data sensitivity and handling requirements."""
|
|
35
|
+
|
|
36
|
+
classification: DataClassification
|
|
37
|
+
categories: list[str] = Field(
|
|
38
|
+
default_factory=list,
|
|
39
|
+
description="Data categories such as PII, PHI, PCI, GDPR, ITAR",
|
|
40
|
+
)
|
|
41
|
+
owner: str = ""
|
|
42
|
+
retention_days: int = 90
|
|
43
|
+
geography: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ABACPolicy(BaseModel):
|
|
47
|
+
"""Attribute-Based Access Control policy for an agent."""
|
|
48
|
+
|
|
49
|
+
agent_id: str
|
|
50
|
+
allowed_classifications: list[DataClassification] = Field(default_factory=list)
|
|
51
|
+
allowed_categories: list[str] = Field(default_factory=list)
|
|
52
|
+
denied_categories: list[str] = Field(default_factory=list)
|
|
53
|
+
required_geography: Optional[str] = None
|
|
54
|
+
max_classification: DataClassification = DataClassification.PUBLIC
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DataAccessDecision(BaseModel):
|
|
58
|
+
"""Result of evaluating an agent's data access request."""
|
|
59
|
+
|
|
60
|
+
allowed: bool
|
|
61
|
+
reason: str
|
|
62
|
+
agent_id: str
|
|
63
|
+
data_label: DataLabel
|
|
64
|
+
matched_policy: Optional[str] = None
|
|
65
|
+
evaluated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DataAccessEvaluator:
|
|
69
|
+
"""Evaluates agent data-access requests against ABAC policies.
|
|
70
|
+
|
|
71
|
+
When multiple policies exist for the same agent, the most restrictive
|
|
72
|
+
result wins (deny takes precedence over allow).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, policies: list[ABACPolicy]) -> None:
|
|
76
|
+
self._policies = policies
|
|
77
|
+
|
|
78
|
+
def evaluate(self, agent_id: str, data_label: DataLabel) -> DataAccessDecision:
|
|
79
|
+
"""Check whether *agent_id* may access data described by *data_label*."""
|
|
80
|
+
agent_policies = [p for p in self._policies if p.agent_id == agent_id]
|
|
81
|
+
if not agent_policies:
|
|
82
|
+
return DataAccessDecision(
|
|
83
|
+
allowed=False,
|
|
84
|
+
reason="No ABAC policy registered for agent",
|
|
85
|
+
agent_id=agent_id,
|
|
86
|
+
data_label=data_label,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Evaluate each policy; any denial means overall denial (most restrictive wins)
|
|
90
|
+
for policy in agent_policies:
|
|
91
|
+
decision = self._evaluate_single(agent_id, data_label, policy)
|
|
92
|
+
if not decision.allowed:
|
|
93
|
+
return decision
|
|
94
|
+
|
|
95
|
+
# All policies passed
|
|
96
|
+
return DataAccessDecision(
|
|
97
|
+
allowed=True,
|
|
98
|
+
reason="Access permitted by all applicable policies",
|
|
99
|
+
agent_id=agent_id,
|
|
100
|
+
data_label=data_label,
|
|
101
|
+
matched_policy=agent_policies[0].agent_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _evaluate_single(
|
|
106
|
+
agent_id: str, data_label: DataLabel, policy: ABACPolicy
|
|
107
|
+
) -> DataAccessDecision:
|
|
108
|
+
policy_ref = policy.agent_id
|
|
109
|
+
|
|
110
|
+
if data_label.classification > policy.max_classification:
|
|
111
|
+
return DataAccessDecision(
|
|
112
|
+
allowed=False,
|
|
113
|
+
reason=(
|
|
114
|
+
f"Classification {data_label.classification.name} exceeds "
|
|
115
|
+
f"max {policy.max_classification.name}"
|
|
116
|
+
),
|
|
117
|
+
agent_id=agent_id,
|
|
118
|
+
data_label=data_label,
|
|
119
|
+
matched_policy=policy_ref,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
policy.allowed_classifications
|
|
124
|
+
and data_label.classification not in policy.allowed_classifications
|
|
125
|
+
):
|
|
126
|
+
return DataAccessDecision(
|
|
127
|
+
allowed=False,
|
|
128
|
+
reason=(
|
|
129
|
+
f"Classification {data_label.classification.name} not in "
|
|
130
|
+
f"allowed list"
|
|
131
|
+
),
|
|
132
|
+
agent_id=agent_id,
|
|
133
|
+
data_label=data_label,
|
|
134
|
+
matched_policy=policy_ref,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for cat in data_label.categories:
|
|
138
|
+
if cat in policy.denied_categories:
|
|
139
|
+
return DataAccessDecision(
|
|
140
|
+
allowed=False,
|
|
141
|
+
reason=f"Category '{cat}' is explicitly denied",
|
|
142
|
+
agent_id=agent_id,
|
|
143
|
+
data_label=data_label,
|
|
144
|
+
matched_policy=policy_ref,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if policy.allowed_categories:
|
|
148
|
+
for cat in data_label.categories:
|
|
149
|
+
if cat not in policy.allowed_categories:
|
|
150
|
+
return DataAccessDecision(
|
|
151
|
+
allowed=False,
|
|
152
|
+
reason=f"Category '{cat}' not in allowed categories",
|
|
153
|
+
agent_id=agent_id,
|
|
154
|
+
data_label=data_label,
|
|
155
|
+
matched_policy=policy_ref,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
policy.required_geography
|
|
160
|
+
and data_label.geography
|
|
161
|
+
and data_label.geography != policy.required_geography
|
|
162
|
+
):
|
|
163
|
+
return DataAccessDecision(
|
|
164
|
+
allowed=False,
|
|
165
|
+
reason=(
|
|
166
|
+
f"Geography '{data_label.geography}' does not match "
|
|
167
|
+
f"required '{policy.required_geography}'"
|
|
168
|
+
),
|
|
169
|
+
agent_id=agent_id,
|
|
170
|
+
data_label=data_label,
|
|
171
|
+
matched_policy=policy_ref,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return DataAccessDecision(
|
|
175
|
+
allowed=True,
|
|
176
|
+
reason="Policy passed",
|
|
177
|
+
agent_id=agent_id,
|
|
178
|
+
data_label=data_label,
|
|
179
|
+
matched_policy=policy_ref,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# PII / PHI / PCI detection helpers
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
_SSN_RE = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
|
|
188
|
+
_EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
|
|
189
|
+
_PHONE_RE = re.compile(
|
|
190
|
+
r"\b(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
_MRN_RE = re.compile(r"\bMRN[-:\s]*\d{6,10}\b", re.IGNORECASE)
|
|
194
|
+
_ICD_RE = re.compile(r"\b[A-Z]\d{2}(?:\.\d{1,4})?\b")
|
|
195
|
+
|
|
196
|
+
_CC_RE = re.compile(r"\b(?:\d[ -]*?){13,19}\b")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def detect_pii(text: str) -> list[str]:
|
|
200
|
+
"""Detect PII patterns (SSN, email, phone) in *text*."""
|
|
201
|
+
findings: list[str] = []
|
|
202
|
+
if _SSN_RE.search(text):
|
|
203
|
+
findings.append("SSN")
|
|
204
|
+
if _EMAIL_RE.search(text):
|
|
205
|
+
findings.append("email")
|
|
206
|
+
if _PHONE_RE.search(text):
|
|
207
|
+
findings.append("phone")
|
|
208
|
+
return findings
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def detect_phi(text: str) -> list[str]:
|
|
212
|
+
"""Detect PHI patterns (medical record numbers, diagnosis codes)."""
|
|
213
|
+
findings: list[str] = []
|
|
214
|
+
if _MRN_RE.search(text):
|
|
215
|
+
findings.append("MRN")
|
|
216
|
+
if _ICD_RE.search(text):
|
|
217
|
+
findings.append("ICD-code")
|
|
218
|
+
return findings
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def detect_pci(text: str) -> list[str]:
|
|
222
|
+
"""Detect PCI patterns (credit card numbers)."""
|
|
223
|
+
findings: list[str] = []
|
|
224
|
+
if _CC_RE.search(text):
|
|
225
|
+
findings.append("credit-card")
|
|
226
|
+
return findings
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def classify_text(text: str) -> DataLabel:
|
|
230
|
+
"""Auto-classify *text* based on detected sensitive-data patterns."""
|
|
231
|
+
categories: list[str] = []
|
|
232
|
+
classification = DataClassification.PUBLIC
|
|
233
|
+
|
|
234
|
+
pii = detect_pii(text)
|
|
235
|
+
if pii:
|
|
236
|
+
categories.append("PII")
|
|
237
|
+
classification = max(classification, DataClassification.CONFIDENTIAL)
|
|
238
|
+
|
|
239
|
+
phi = detect_phi(text)
|
|
240
|
+
if phi:
|
|
241
|
+
categories.append("PHI")
|
|
242
|
+
classification = max(classification, DataClassification.RESTRICTED)
|
|
243
|
+
|
|
244
|
+
pci = detect_pci(text)
|
|
245
|
+
if pci:
|
|
246
|
+
categories.append("PCI")
|
|
247
|
+
classification = max(classification, DataClassification.CONFIDENTIAL)
|
|
248
|
+
|
|
249
|
+
if not categories:
|
|
250
|
+
classification = DataClassification.PUBLIC
|
|
251
|
+
|
|
252
|
+
return DataLabel(classification=classification, categories=categories)
|