admina-framework 0.9.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.
Files changed (102) hide show
  1. admina/__init__.py +34 -0
  2. admina/cli/__init__.py +14 -0
  3. admina/cli/commands/__init__.py +14 -0
  4. admina/cli/main.py +1522 -0
  5. admina/cli/templates/admina.yaml.j2 +77 -0
  6. admina/cli/templates/docker-compose.yml.j2 +254 -0
  7. admina/cli/templates/env.j2 +10 -0
  8. admina/cli/templates/main.py.j2 +95 -0
  9. admina/cli/templates/plugin.py.j2 +145 -0
  10. admina/cli/templates/plugin_pyproject.toml.j2 +15 -0
  11. admina/cli/templates/plugin_readme.md.j2 +27 -0
  12. admina/cli/templates/plugin_test.py.j2 +48 -0
  13. admina/core/__init__.py +14 -0
  14. admina/core/config.py +497 -0
  15. admina/core/event_bus.py +112 -0
  16. admina/core/secrets.py +257 -0
  17. admina/core/types.py +146 -0
  18. admina/dashboard/__init__.py +8 -0
  19. admina/dashboard/static/heimdall.png +0 -0
  20. admina/dashboard/static/index.html +1045 -0
  21. admina/dashboard/static/vendor/alpinejs.min.js +5 -0
  22. admina/domains/__init__.py +14 -0
  23. admina/domains/agent_security/__init__.py +41 -0
  24. admina/domains/agent_security/firewall.py +634 -0
  25. admina/domains/agent_security/loop_breaker.py +176 -0
  26. admina/domains/ai_infra/__init__.py +79 -0
  27. admina/domains/ai_infra/llm_engine.py +477 -0
  28. admina/domains/ai_infra/rag.py +817 -0
  29. admina/domains/ai_infra/webui.py +292 -0
  30. admina/domains/compliance/__init__.py +109 -0
  31. admina/domains/compliance/cross_regulation.py +314 -0
  32. admina/domains/compliance/eu_ai_act.py +367 -0
  33. admina/domains/compliance/forensic.py +380 -0
  34. admina/domains/compliance/gdpr.py +331 -0
  35. admina/domains/compliance/nis2.py +258 -0
  36. admina/domains/compliance/oisg.py +658 -0
  37. admina/domains/compliance/otel.py +101 -0
  38. admina/domains/data_sovereignty/__init__.py +42 -0
  39. admina/domains/data_sovereignty/classification.py +102 -0
  40. admina/domains/data_sovereignty/pii.py +260 -0
  41. admina/domains/data_sovereignty/residency.py +121 -0
  42. admina/integrations/__init__.py +14 -0
  43. admina/integrations/_engines.py +63 -0
  44. admina/integrations/cheshirecat/__init__.py +13 -0
  45. admina/integrations/cheshirecat/admina-plugin/admina_governance.py +207 -0
  46. admina/integrations/crewai/__init__.py +13 -0
  47. admina/integrations/crewai/callbacks.py +347 -0
  48. admina/integrations/langchain/__init__.py +13 -0
  49. admina/integrations/langchain/callbacks.py +341 -0
  50. admina/integrations/n8n/__init__.py +14 -0
  51. admina/integrations/openclaw/__init__.py +14 -0
  52. admina/plugins/__init__.py +49 -0
  53. admina/plugins/base.py +633 -0
  54. admina/plugins/builtin/__init__.py +14 -0
  55. admina/plugins/builtin/adapters/__init__.py +14 -0
  56. admina/plugins/builtin/adapters/ollama.py +120 -0
  57. admina/plugins/builtin/adapters/openai.py +138 -0
  58. admina/plugins/builtin/alerts/__init__.py +14 -0
  59. admina/plugins/builtin/alerts/log.py +66 -0
  60. admina/plugins/builtin/alerts/webhook.py +102 -0
  61. admina/plugins/builtin/auth/__init__.py +14 -0
  62. admina/plugins/builtin/auth/apikey.py +138 -0
  63. admina/plugins/builtin/compliance/__init__.py +14 -0
  64. admina/plugins/builtin/compliance/eu_ai_act.py +202 -0
  65. admina/plugins/builtin/connectors/__init__.py +14 -0
  66. admina/plugins/builtin/connectors/chromadb.py +137 -0
  67. admina/plugins/builtin/connectors/filesystem.py +111 -0
  68. admina/plugins/builtin/forensic/__init__.py +14 -0
  69. admina/plugins/builtin/forensic/filesystem.py +163 -0
  70. admina/plugins/builtin/forensic/minio.py +180 -0
  71. admina/plugins/builtin/guards/__init__.py +0 -0
  72. admina/plugins/builtin/guards/guardrailsai_guard.py +172 -0
  73. admina/plugins/builtin/pii/__init__.py +14 -0
  74. admina/plugins/builtin/pii/spacy_regex.py +160 -0
  75. admina/plugins/builtin/transports/__init__.py +14 -0
  76. admina/plugins/builtin/transports/http_rest.py +97 -0
  77. admina/plugins/builtin/transports/mcp.py +173 -0
  78. admina/plugins/registry.py +356 -0
  79. admina/proxy/__init__.py +15 -0
  80. admina/proxy/api/__init__.py +17 -0
  81. admina/proxy/api/dashboard.py +925 -0
  82. admina/proxy/api/integration.py +153 -0
  83. admina/proxy/config.py +214 -0
  84. admina/proxy/engine_bridge.py +306 -0
  85. admina/proxy/governance.py +232 -0
  86. admina/proxy/main.py +1484 -0
  87. admina/proxy/multi_upstream.py +156 -0
  88. admina/proxy/state.py +97 -0
  89. admina/py.typed +0 -0
  90. admina/sdk/__init__.py +34 -0
  91. admina/sdk/_compat.py +43 -0
  92. admina/sdk/compliance_kit.py +359 -0
  93. admina/sdk/governed_agent.py +391 -0
  94. admina/sdk/governed_data.py +434 -0
  95. admina/sdk/governed_model.py +241 -0
  96. admina_framework-0.9.0.dist-info/METADATA +575 -0
  97. admina_framework-0.9.0.dist-info/RECORD +102 -0
  98. admina_framework-0.9.0.dist-info/WHEEL +5 -0
  99. admina_framework-0.9.0.dist-info/entry_points.txt +2 -0
  100. admina_framework-0.9.0.dist-info/licenses/LICENSE +191 -0
  101. admina_framework-0.9.0.dist-info/licenses/NOTICE +16 -0
  102. admina_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,156 @@
1
+ # Copyright © 2025–2026 Stefano Noferi & Admina contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Admina — Multi-Upstream MCP Router
17
+ Routes MCP calls to correct upstream servers. Enables OpenClaw integration.
18
+ """
19
+
20
+ import json
21
+ import logging
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+
25
+ logger = logging.getLogger("admina.router")
26
+
27
+
28
+ @dataclass
29
+ class ServerRoute:
30
+ """A registered upstream MCP server."""
31
+
32
+ name: str
33
+ upstream_url: str = ""
34
+ server_type: str = "sse" # sse | http | stdio
35
+ proxy_mode: str = "transparent" # transparent | stdio_wrap
36
+ headers: dict = field(default_factory=dict)
37
+ tool_names: list = field(default_factory=list) # tools this server provides
38
+ healthy: bool = True
39
+ request_count: int = 0
40
+ block_count: int = 0
41
+
42
+
43
+ class MultiUpstreamRouter:
44
+ """
45
+ Manages routing to multiple MCP upstream servers.
46
+
47
+ Loading:
48
+ router = MultiUpstreamRouter()
49
+ router.load_config("/app/routing.json")
50
+
51
+ Routing:
52
+ route = router.resolve("github") # by server name
53
+ route = router.resolve_by_tool("git_push") # by tool name (after discovery)
54
+ """
55
+
56
+ def __init__(self, default_upstream: str = "http://localhost:9000"):
57
+ self.default_upstream = default_upstream
58
+ self.routes: dict[str, ServerRoute] = {}
59
+ self.tool_to_server: dict[str, str] = {}
60
+ self.governance_config: dict = {}
61
+ self._loaded = False
62
+
63
+ def load_config(self, config_path: str | Path) -> None:
64
+ """Load routing configuration from JSON file."""
65
+ path = Path(config_path)
66
+ if not path.exists():
67
+ logger.warning("Routing config not found: %s, using default upstream", path)
68
+ return
69
+
70
+ with open(path) as f:
71
+ config = json.load(f)
72
+
73
+ self.default_upstream = config.get("default_upstream", self.default_upstream)
74
+ self.governance_config = config.get("governance", {})
75
+
76
+ for name, route_def in config.get("routes", {}).items():
77
+ self.routes[name] = ServerRoute(
78
+ name=name,
79
+ upstream_url=route_def.get("upstream_url", ""),
80
+ server_type=route_def.get("type", "sse"),
81
+ proxy_mode=route_def.get("proxy_mode", "transparent"),
82
+ headers=route_def.get("headers", {}),
83
+ )
84
+
85
+ self._loaded = True
86
+ logger.info("Loaded %d upstream routes from %s", len(self.routes), path)
87
+
88
+ def resolve(self, server_name: str) -> ServerRoute | None:
89
+ """Resolve a server route by name."""
90
+ route = self.routes.get(server_name)
91
+ if route:
92
+ route.request_count += 1
93
+ return route
94
+
95
+ def resolve_by_tool(self, tool_name: str) -> ServerRoute | None:
96
+ """Resolve which server provides a given tool."""
97
+ server_name = self.tool_to_server.get(tool_name)
98
+ if server_name:
99
+ return self.resolve(server_name)
100
+ return None
101
+
102
+ def register_tool_mapping(self, server_name: str, tool_names: list[str]) -> None:
103
+ """After tool discovery, map tool names to their server."""
104
+ for tool in tool_names:
105
+ self.tool_to_server[tool] = server_name
106
+ route = self.routes.get(server_name)
107
+ if route:
108
+ route.tool_names = tool_names
109
+ logger.info("Registered %d tools for server '%s'", len(tool_names), server_name)
110
+
111
+ def get_upstream_url(self, server_name: str | None = None) -> str:
112
+ """Get the upstream URL for a given server, or default."""
113
+ if server_name:
114
+ route = self.resolve(server_name)
115
+ if route and route.upstream_url:
116
+ return route.upstream_url
117
+ return self.default_upstream
118
+
119
+ def get_upstream_headers(self, server_name: str | None = None) -> dict:
120
+ """Get any extra headers for the upstream server."""
121
+ if server_name:
122
+ route = self.resolve(server_name)
123
+ if route:
124
+ return route.headers
125
+ return {}
126
+
127
+ def record_block(self, server_name: str) -> None:
128
+ """Record that a request to this server was blocked."""
129
+ route = self.routes.get(server_name)
130
+ if route:
131
+ route.block_count += 1
132
+
133
+ def get_stats(self) -> dict:
134
+ """Return routing statistics."""
135
+ return {
136
+ "total_routes": len(self.routes),
137
+ "loaded": self._loaded,
138
+ "default_upstream": self.default_upstream,
139
+ "routes": {
140
+ name: {
141
+ "type": r.server_type,
142
+ "upstream": r.upstream_url or "(stdio)",
143
+ "requests": r.request_count,
144
+ "blocks": r.block_count,
145
+ "tools": len(r.tool_names),
146
+ "healthy": r.healthy,
147
+ }
148
+ for name, r in self.routes.items()
149
+ },
150
+ "tool_mappings": len(self.tool_to_server),
151
+ }
152
+
153
+ @property
154
+ def is_multi_upstream(self) -> bool:
155
+ """Whether multi-upstream mode is active."""
156
+ return self._loaded and len(self.routes) > 0
admina/proxy/state.py ADDED
@@ -0,0 +1,97 @@
1
+ # Copyright © 2025–2026 Stefano Noferi & Admina contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Proxy runtime state — holds connections, engines, and metrics."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import threading
20
+ from dataclasses import dataclass, field
21
+ from datetime import UTC, datetime
22
+ from typing import Any
23
+
24
+ import httpx
25
+ import redis.asyncio as aioredis
26
+ from minio import Minio
27
+
28
+ from admina.domains.compliance.eu_ai_act import EUAIActCompliance
29
+ from admina.domains.compliance.forensic import ForensicBlackBox
30
+ from admina.domains.compliance.gdpr import ProcessingActivitiesRegistry
31
+ from admina.domains.compliance.nis2 import NIS2Compliance
32
+ from admina.domains.compliance.otel import OTELGovernanceExporter
33
+ from admina.plugins.registry import PluginRegistry
34
+ from admina.proxy.multi_upstream import MultiUpstreamRouter
35
+
36
+
37
+ @dataclass
38
+ class ProxyState:
39
+ """Mutable runtime state for the proxy.
40
+
41
+ Created once at startup, passed to handlers via app.state.
42
+ """
43
+
44
+ # Connections
45
+ redis: aioredis.Redis | None = None
46
+ minio: Minio | None = None
47
+ clickhouse: Any = None
48
+ http_client: httpx.AsyncClient | None = None
49
+
50
+ # Governance engines (set by engine_bridge)
51
+ firewall: Any = None
52
+ pii_redactor: Any = None
53
+ loop_breaker: Any = None
54
+
55
+ # Subsystems
56
+ forensic_box: ForensicBlackBox | None = None
57
+ compliance: EUAIActCompliance = field(default_factory=EUAIActCompliance)
58
+ nis2: NIS2Compliance = field(default_factory=NIS2Compliance)
59
+ gdpr: ProcessingActivitiesRegistry = field(default_factory=ProcessingActivitiesRegistry)
60
+ router: MultiUpstreamRouter | None = None
61
+ registry: PluginRegistry = field(default_factory=PluginRegistry)
62
+
63
+ # Plugins
64
+ governance_guards: list = field(default_factory=list)
65
+ alert_channels: list = field(default_factory=list)
66
+ auth_providers: list = field(default_factory=list)
67
+
68
+ # OTEL
69
+ otel_exporter: OTELGovernanceExporter | None = None
70
+
71
+ # Metrics
72
+ metrics: dict[str, Any] = field(
73
+ default_factory=lambda: {
74
+ "requests_total": 0,
75
+ "requests_blocked": 0,
76
+ "requests_allowed": 0,
77
+ "requests_redacted": 0,
78
+ "avg_latency_ms": 0.0,
79
+ "started_at": datetime.now(UTC).isoformat(),
80
+ }
81
+ )
82
+ _metrics_lock: threading.Lock = field(default_factory=threading.Lock)
83
+
84
+ def inc_metric(self, key: str, value: int = 1) -> None:
85
+ with self._metrics_lock:
86
+ self.metrics[key] += value
87
+
88
+ def update_avg_latency(self, latency_ms: float) -> None:
89
+ with self._metrics_lock:
90
+ n = self.metrics["requests_total"]
91
+ if n <= 1:
92
+ self.metrics["avg_latency_ms"] = latency_ms
93
+ else:
94
+ self.metrics["avg_latency_ms"] = round(
95
+ (self.metrics["avg_latency_ms"] * (n - 1) + latency_ms) / n,
96
+ 2,
97
+ )
admina/py.typed ADDED
File without changes
admina/sdk/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ # Copyright © 2025–2026 Stefano Noferi & Admina contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Admina SDK — governed AI primitives.
16
+
17
+ Usage::
18
+
19
+ from admina.sdk import GovernedModel, GovernedData, GovernedAgent, ComplianceKit
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from admina.sdk.compliance_kit import ComplianceKit
25
+ from admina.sdk.governed_agent import GovernedAgent
26
+ from admina.sdk.governed_data import GovernedData
27
+ from admina.sdk.governed_model import GovernedModel
28
+
29
+ __all__ = [
30
+ "GovernedModel",
31
+ "GovernedData",
32
+ "GovernedAgent",
33
+ "ComplianceKit",
34
+ ]
admina/sdk/_compat.py ADDED
@@ -0,0 +1,43 @@
1
+ # Copyright © 2025–2026 Stefano Noferi & Admina contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Async-to-sync bridge that works both inside and outside running loops."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ from collections.abc import Coroutine
21
+ from typing import Any, TypeVar
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ def run_sync(coro: Coroutine[Any, Any, T]) -> T:
27
+ """Run an async coroutine synchronously.
28
+
29
+ Works correctly whether or not an event loop is already running.
30
+ """
31
+ try:
32
+ asyncio.get_running_loop()
33
+ except RuntimeError:
34
+ # No running loop — safe to use asyncio.run()
35
+ return asyncio.run(coro)
36
+ else:
37
+ # Inside a running loop (Jupyter, FastAPI, etc.)
38
+ # Create a new thread to avoid deadlock
39
+ import concurrent.futures
40
+
41
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
42
+ future = pool.submit(asyncio.run, coro)
43
+ return future.result()
@@ -0,0 +1,359 @@
1
+ # Copyright © 2025–2026 Stefano Noferi & Admina contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Admina — ComplianceKit SDK primitive.
16
+
17
+ Wraps the existing EU AI Act compliance engine and provides a unified
18
+ interface for risk classification, gap analysis, and report generation
19
+ across compliance frameworks.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from dataclasses import dataclass, field
26
+ from typing import Any
27
+
28
+ from admina.core.event_bus import EventType, GovernanceEvent, bus
29
+ from admina.domains.compliance.eu_ai_act import EUAIActCompliance
30
+ from admina.sdk._compat import run_sync
31
+
32
+ __all__ = ["ComplianceKit"]
33
+
34
+
35
+ @dataclass
36
+ class RiskClassification:
37
+ """Result of a risk classification.
38
+
39
+ Attributes:
40
+ risk_category: The risk category (unacceptable, high, limited, minimal).
41
+ level: Numeric risk level (1-4).
42
+ description: Human-readable description.
43
+ action: Required action for this risk level.
44
+ framework: The compliance framework used.
45
+ """
46
+
47
+ risk_category: str
48
+ level: int = 1
49
+ description: str = ""
50
+ action: str = ""
51
+ framework: str = "eu_ai_act"
52
+
53
+
54
+ @dataclass
55
+ class GapReport:
56
+ """Result of a gap analysis.
57
+
58
+ Attributes:
59
+ applicable: Whether gap analysis applies to this risk level.
60
+ compliance_score: Percentage of checks passed (0-100).
61
+ total_checks: Total number of checks evaluated.
62
+ passed_checks: Number of checks passed.
63
+ gaps: List of unmet requirements.
64
+ status: COMPLIANT or GAPS_FOUND.
65
+ framework: The compliance framework used.
66
+ message: Optional message (e.g. for non-applicable cases).
67
+ """
68
+
69
+ applicable: bool = False
70
+ compliance_score: float = 0.0
71
+ total_checks: int = 0
72
+ passed_checks: int = 0
73
+ gaps: list[dict[str, Any]] = field(default_factory=list)
74
+ status: str = "GAPS_FOUND"
75
+ framework: str = "eu_ai_act"
76
+ message: str = ""
77
+
78
+
79
+ @dataclass
80
+ class Report:
81
+ """A generated compliance report.
82
+
83
+ Attributes:
84
+ title: Report title.
85
+ generated_at: ISO timestamp of generation.
86
+ framework: The compliance framework.
87
+ classification: Risk classification details.
88
+ gap_analysis: Gap analysis details.
89
+ recommendations: List of recommended actions.
90
+ content: Full report content dict.
91
+ """
92
+
93
+ title: str = ""
94
+ generated_at: str = ""
95
+ framework: str = "eu_ai_act"
96
+ classification: dict[str, Any] = field(default_factory=dict)
97
+ gap_analysis: dict[str, Any] = field(default_factory=dict)
98
+ recommendations: list[str] = field(default_factory=list)
99
+ content: dict[str, Any] = field(default_factory=dict)
100
+
101
+ def to_json(self) -> str:
102
+ """Serialize the report to JSON string.
103
+
104
+ Returns:
105
+ JSON string of the report content.
106
+ """
107
+ return json.dumps(self.content, indent=2, default=str)
108
+
109
+
110
+ class ComplianceKit:
111
+ """SDK primitive for compliance checking and reporting.
112
+
113
+ Provides risk classification, gap analysis, and report generation
114
+ across compliance frameworks. Currently supports EU AI Act with
115
+ extensibility for additional frameworks via plugin templates.
116
+
117
+ Args:
118
+ frameworks: List of framework names to activate.
119
+ Defaults to ["eu_ai_act"].
120
+ audit: Whether to emit governance events.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ frameworks: list[str] | None = None,
126
+ audit: bool = True,
127
+ ) -> None:
128
+ """Initialize ComplianceKit.
129
+
130
+ Args:
131
+ frameworks: Framework names to activate. Defaults to ["eu_ai_act"].
132
+ audit: If True, emit events to the event bus.
133
+ """
134
+ self._frameworks = frameworks or ["eu_ai_act"]
135
+ self._audit = audit
136
+ self._engines: dict[str, Any] = {}
137
+ self._init_engines()
138
+
139
+ def _init_engines(self) -> None:
140
+ """Initialize compliance engines for configured frameworks."""
141
+ for fw in self._frameworks:
142
+ if fw == "eu_ai_act":
143
+ self._engines[fw] = EUAIActCompliance()
144
+
145
+ def _get_engine(self, framework: str) -> Any:
146
+ """Get the compliance engine for a framework.
147
+
148
+ Args:
149
+ framework: Framework name.
150
+
151
+ Returns:
152
+ The compliance engine instance.
153
+
154
+ Raises:
155
+ ValueError: If the framework is not supported.
156
+ """
157
+ engine = self._engines.get(framework)
158
+ if engine is None:
159
+ raise ValueError(
160
+ f"Framework '{framework}' is not supported. Available: {list(self._engines.keys())}"
161
+ )
162
+ return engine
163
+
164
+ def classify_risk(
165
+ self,
166
+ description: str,
167
+ use_case: str,
168
+ data_types: list[str] | None = None,
169
+ framework: str = "eu_ai_act",
170
+ ) -> RiskClassification:
171
+ """Classify an AI system's risk level.
172
+
173
+ Args:
174
+ description: Description of the AI system.
175
+ use_case: The use case or application domain.
176
+ data_types: Types of data processed (e.g. ["health", "financial"]).
177
+ framework: Compliance framework to use.
178
+
179
+ Returns:
180
+ RiskClassification with category, level, and required action.
181
+ """
182
+ engine = self._get_engine(framework)
183
+ result = engine.classify_risk(
184
+ description,
185
+ use_case,
186
+ data_types or [],
187
+ )
188
+
189
+ classification = RiskClassification(
190
+ risk_category=result["risk_category"],
191
+ level=result.get("level", 1),
192
+ description=result.get("description", ""),
193
+ action=result.get("action", ""),
194
+ framework=framework,
195
+ )
196
+
197
+ if self._audit:
198
+ _emit_sync(
199
+ GovernanceEvent(
200
+ event_type=EventType.COMPLIANCE_CHECK,
201
+ domain="compliance",
202
+ action="ALLOW",
203
+ metadata={
204
+ "operation": "classify_risk",
205
+ "framework": framework,
206
+ "risk_category": classification.risk_category,
207
+ "level": classification.level,
208
+ },
209
+ )
210
+ )
211
+
212
+ return classification
213
+
214
+ def gap_analysis(
215
+ self,
216
+ framework: str = "eu_ai_act",
217
+ risk_category: str = "high",
218
+ current_compliance: dict[str, list[bool] | bool] | None = None,
219
+ ) -> GapReport:
220
+ """Perform a compliance gap analysis.
221
+
222
+ Args:
223
+ framework: Compliance framework to evaluate.
224
+ risk_category: The risk category to analyze gaps for.
225
+ current_compliance: Dict mapping requirement keys to either
226
+ a list of booleans (one per check) or a single boolean
227
+ (treated as a single check that is fully met or unmet).
228
+
229
+ Returns:
230
+ GapReport with score, gaps, and status.
231
+
232
+ Raises:
233
+ ValueError: If a value is not a bool or list of bools.
234
+ """
235
+ engine = self._get_engine(framework)
236
+ normalised: dict[str, list[bool]] = {}
237
+ for key, value in (current_compliance or {}).items():
238
+ if isinstance(value, bool):
239
+ normalised[key] = [value]
240
+ elif isinstance(value, list) and all(isinstance(v, bool) for v in value):
241
+ normalised[key] = value
242
+ else:
243
+ raise ValueError(
244
+ f"current_compliance[{key!r}] must be bool or list[bool], "
245
+ f"got {type(value).__name__}"
246
+ )
247
+ result = engine.gap_analysis(risk_category, normalised)
248
+
249
+ report = GapReport(
250
+ applicable=result.get("applicable", False),
251
+ compliance_score=result.get("compliance_score", 0.0),
252
+ total_checks=result.get("total_checks", 0),
253
+ passed_checks=result.get("passed_checks", 0),
254
+ gaps=result.get("gaps", []),
255
+ status=result.get("status", "GAPS_FOUND"),
256
+ framework=framework,
257
+ message=result.get("message", ""),
258
+ )
259
+
260
+ if self._audit:
261
+ _emit_sync(
262
+ GovernanceEvent(
263
+ event_type=EventType.COMPLIANCE_CHECK,
264
+ domain="compliance",
265
+ metadata={
266
+ "operation": "gap_analysis",
267
+ "framework": framework,
268
+ "applicable": report.applicable,
269
+ "compliance_score": report.compliance_score,
270
+ "gap_count": len(report.gaps),
271
+ },
272
+ )
273
+ )
274
+
275
+ return report
276
+
277
+ def generate_report(
278
+ self,
279
+ system_name: str,
280
+ description: str,
281
+ use_case: str,
282
+ data_types: list[str] | None = None,
283
+ current_compliance: dict[str, list[bool]] | None = None,
284
+ framework: str = "eu_ai_act",
285
+ ) -> Report:
286
+ """Generate a full compliance report.
287
+
288
+ Runs risk classification and gap analysis, then produces a
289
+ structured report with recommendations.
290
+
291
+ Args:
292
+ system_name: Name of the AI system.
293
+ description: Description of the AI system.
294
+ use_case: The use case or application domain.
295
+ data_types: Types of data processed.
296
+ current_compliance: Current compliance state for gap analysis.
297
+ framework: Compliance framework to use.
298
+
299
+ Returns:
300
+ Report with classification, gaps, and recommendations.
301
+ """
302
+ engine = self._get_engine(framework)
303
+
304
+ classification = engine.classify_risk(
305
+ description,
306
+ use_case,
307
+ data_types or [],
308
+ )
309
+ gap_result = engine.gap_analysis(
310
+ classification["risk_category"],
311
+ current_compliance or {},
312
+ )
313
+ report_data = engine.generate_report(
314
+ system_name,
315
+ classification,
316
+ gap_result,
317
+ )
318
+
319
+ report = Report(
320
+ title=report_data.get("report_title", ""),
321
+ generated_at=report_data.get("generated_at", ""),
322
+ framework=framework,
323
+ classification=classification,
324
+ gap_analysis=gap_result,
325
+ recommendations=report_data.get("recommendations", []),
326
+ content=report_data,
327
+ )
328
+
329
+ if self._audit:
330
+ _emit_sync(
331
+ GovernanceEvent(
332
+ event_type=EventType.COMPLIANCE_CHECK,
333
+ domain="compliance",
334
+ metadata={
335
+ "operation": "generate_report",
336
+ "framework": framework,
337
+ "system_name": system_name,
338
+ "risk_category": classification["risk_category"],
339
+ },
340
+ )
341
+ )
342
+
343
+ return report
344
+
345
+ @property
346
+ def supported_frameworks(self) -> list[str]:
347
+ """Return list of supported framework names."""
348
+ return list(self._engines.keys())
349
+
350
+
351
+ def _emit_sync(event: GovernanceEvent) -> None:
352
+ """Emit an event synchronously.
353
+
354
+ Uses an event loop if available, otherwise creates one.
355
+
356
+ Args:
357
+ event: The governance event to emit.
358
+ """
359
+ run_sync(bus.emit(event))