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.
- admina/__init__.py +34 -0
- admina/cli/__init__.py +14 -0
- admina/cli/commands/__init__.py +14 -0
- admina/cli/main.py +1522 -0
- admina/cli/templates/admina.yaml.j2 +77 -0
- admina/cli/templates/docker-compose.yml.j2 +254 -0
- admina/cli/templates/env.j2 +10 -0
- admina/cli/templates/main.py.j2 +95 -0
- admina/cli/templates/plugin.py.j2 +145 -0
- admina/cli/templates/plugin_pyproject.toml.j2 +15 -0
- admina/cli/templates/plugin_readme.md.j2 +27 -0
- admina/cli/templates/plugin_test.py.j2 +48 -0
- admina/core/__init__.py +14 -0
- admina/core/config.py +497 -0
- admina/core/event_bus.py +112 -0
- admina/core/secrets.py +257 -0
- admina/core/types.py +146 -0
- admina/dashboard/__init__.py +8 -0
- admina/dashboard/static/heimdall.png +0 -0
- admina/dashboard/static/index.html +1045 -0
- admina/dashboard/static/vendor/alpinejs.min.js +5 -0
- admina/domains/__init__.py +14 -0
- admina/domains/agent_security/__init__.py +41 -0
- admina/domains/agent_security/firewall.py +634 -0
- admina/domains/agent_security/loop_breaker.py +176 -0
- admina/domains/ai_infra/__init__.py +79 -0
- admina/domains/ai_infra/llm_engine.py +477 -0
- admina/domains/ai_infra/rag.py +817 -0
- admina/domains/ai_infra/webui.py +292 -0
- admina/domains/compliance/__init__.py +109 -0
- admina/domains/compliance/cross_regulation.py +314 -0
- admina/domains/compliance/eu_ai_act.py +367 -0
- admina/domains/compliance/forensic.py +380 -0
- admina/domains/compliance/gdpr.py +331 -0
- admina/domains/compliance/nis2.py +258 -0
- admina/domains/compliance/oisg.py +658 -0
- admina/domains/compliance/otel.py +101 -0
- admina/domains/data_sovereignty/__init__.py +42 -0
- admina/domains/data_sovereignty/classification.py +102 -0
- admina/domains/data_sovereignty/pii.py +260 -0
- admina/domains/data_sovereignty/residency.py +121 -0
- admina/integrations/__init__.py +14 -0
- admina/integrations/_engines.py +63 -0
- admina/integrations/cheshirecat/__init__.py +13 -0
- admina/integrations/cheshirecat/admina-plugin/admina_governance.py +207 -0
- admina/integrations/crewai/__init__.py +13 -0
- admina/integrations/crewai/callbacks.py +347 -0
- admina/integrations/langchain/__init__.py +13 -0
- admina/integrations/langchain/callbacks.py +341 -0
- admina/integrations/n8n/__init__.py +14 -0
- admina/integrations/openclaw/__init__.py +14 -0
- admina/plugins/__init__.py +49 -0
- admina/plugins/base.py +633 -0
- admina/plugins/builtin/__init__.py +14 -0
- admina/plugins/builtin/adapters/__init__.py +14 -0
- admina/plugins/builtin/adapters/ollama.py +120 -0
- admina/plugins/builtin/adapters/openai.py +138 -0
- admina/plugins/builtin/alerts/__init__.py +14 -0
- admina/plugins/builtin/alerts/log.py +66 -0
- admina/plugins/builtin/alerts/webhook.py +102 -0
- admina/plugins/builtin/auth/__init__.py +14 -0
- admina/plugins/builtin/auth/apikey.py +138 -0
- admina/plugins/builtin/compliance/__init__.py +14 -0
- admina/plugins/builtin/compliance/eu_ai_act.py +202 -0
- admina/plugins/builtin/connectors/__init__.py +14 -0
- admina/plugins/builtin/connectors/chromadb.py +137 -0
- admina/plugins/builtin/connectors/filesystem.py +111 -0
- admina/plugins/builtin/forensic/__init__.py +14 -0
- admina/plugins/builtin/forensic/filesystem.py +163 -0
- admina/plugins/builtin/forensic/minio.py +180 -0
- admina/plugins/builtin/guards/__init__.py +0 -0
- admina/plugins/builtin/guards/guardrailsai_guard.py +172 -0
- admina/plugins/builtin/pii/__init__.py +14 -0
- admina/plugins/builtin/pii/spacy_regex.py +160 -0
- admina/plugins/builtin/transports/__init__.py +14 -0
- admina/plugins/builtin/transports/http_rest.py +97 -0
- admina/plugins/builtin/transports/mcp.py +173 -0
- admina/plugins/registry.py +356 -0
- admina/proxy/__init__.py +15 -0
- admina/proxy/api/__init__.py +17 -0
- admina/proxy/api/dashboard.py +925 -0
- admina/proxy/api/integration.py +153 -0
- admina/proxy/config.py +214 -0
- admina/proxy/engine_bridge.py +306 -0
- admina/proxy/governance.py +232 -0
- admina/proxy/main.py +1484 -0
- admina/proxy/multi_upstream.py +156 -0
- admina/proxy/state.py +97 -0
- admina/py.typed +0 -0
- admina/sdk/__init__.py +34 -0
- admina/sdk/_compat.py +43 -0
- admina/sdk/compliance_kit.py +359 -0
- admina/sdk/governed_agent.py +391 -0
- admina/sdk/governed_data.py +434 -0
- admina/sdk/governed_model.py +241 -0
- admina_framework-0.9.0.dist-info/METADATA +575 -0
- admina_framework-0.9.0.dist-info/RECORD +102 -0
- admina_framework-0.9.0.dist-info/WHEEL +5 -0
- admina_framework-0.9.0.dist-info/entry_points.txt +2 -0
- admina_framework-0.9.0.dist-info/licenses/LICENSE +191 -0
- admina_framework-0.9.0.dist-info/licenses/NOTICE +16 -0
- admina_framework-0.9.0.dist-info/top_level.txt +1 -0
admina/core/config.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
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 — Configuration loader.
|
|
16
|
+
|
|
17
|
+
Reads ``admina.yaml`` if present, falls back to ``.env`` variables for
|
|
18
|
+
backward compatibility. Exposes a typed :class:`AdminaConfig` object.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("admina.config")
|
|
30
|
+
|
|
31
|
+
# PyYAML is an optional dependency — fall back gracefully.
|
|
32
|
+
try:
|
|
33
|
+
import yaml # type: ignore[import-untyped]
|
|
34
|
+
|
|
35
|
+
_HAS_YAML = True
|
|
36
|
+
except ImportError: # pragma: no cover
|
|
37
|
+
_HAS_YAML = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["AdminaConfig", "load_config"]
|
|
41
|
+
|
|
42
|
+
# ── Section dataclasses ──────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class PIIConfig:
|
|
47
|
+
"""PII redaction settings."""
|
|
48
|
+
|
|
49
|
+
enabled: bool = True
|
|
50
|
+
categories: list[str] = field(
|
|
51
|
+
default_factory=lambda: [
|
|
52
|
+
"email",
|
|
53
|
+
"phone",
|
|
54
|
+
"credit_card",
|
|
55
|
+
"ssn",
|
|
56
|
+
"iban",
|
|
57
|
+
"ip",
|
|
58
|
+
"person",
|
|
59
|
+
"org",
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
ner_model: str = "en_core_web_sm"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ResidencyConfig:
|
|
67
|
+
"""Data residency settings."""
|
|
68
|
+
|
|
69
|
+
enabled: bool = True
|
|
70
|
+
allowed_zones: list[str] = field(default_factory=lambda: ["local", "eu"])
|
|
71
|
+
block_outbound: bool = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class DataSovereigntyConfig:
|
|
76
|
+
"""Data-sovereignty domain."""
|
|
77
|
+
|
|
78
|
+
enabled: bool = True
|
|
79
|
+
pii: PIIConfig = field(default_factory=PIIConfig)
|
|
80
|
+
residency: ResidencyConfig = field(default_factory=ResidencyConfig)
|
|
81
|
+
classification_enabled: bool = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class LLMConfig:
|
|
86
|
+
"""LLM backend settings."""
|
|
87
|
+
|
|
88
|
+
enabled: bool = True
|
|
89
|
+
backend: str = "ollama"
|
|
90
|
+
model: str = "llama3.1:8b"
|
|
91
|
+
gpu_autodetect: bool = True
|
|
92
|
+
vram_limit_mb: int = 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class RAGConfig:
|
|
97
|
+
"""RAG pipeline settings."""
|
|
98
|
+
|
|
99
|
+
enabled: bool = True
|
|
100
|
+
backend: str = "chromadb"
|
|
101
|
+
chunk_size: int = 512
|
|
102
|
+
chunk_overlap: int = 50
|
|
103
|
+
embedding_backend: str = "ollama"
|
|
104
|
+
embedding_model: str = "nomic-embed-text"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class WebUIConfig:
|
|
109
|
+
"""Web UI settings."""
|
|
110
|
+
|
|
111
|
+
enabled: bool = True
|
|
112
|
+
port: int = 3080
|
|
113
|
+
auth_mode: str = "builtin"
|
|
114
|
+
signup_enabled: bool = True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class AIInfraConfig:
|
|
119
|
+
"""AI-infra domain (opt-in)."""
|
|
120
|
+
|
|
121
|
+
enabled: bool = False
|
|
122
|
+
llm: LLMConfig = field(default_factory=LLMConfig)
|
|
123
|
+
rag: RAGConfig = field(default_factory=RAGConfig)
|
|
124
|
+
webui: WebUIConfig = field(default_factory=WebUIConfig)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class ProxyConfig:
|
|
129
|
+
"""Proxy settings."""
|
|
130
|
+
|
|
131
|
+
port: int = 8080
|
|
132
|
+
upstream: str = "http://localhost:9000"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class FirewallConfig:
|
|
137
|
+
"""Anti-injection firewall settings."""
|
|
138
|
+
|
|
139
|
+
enabled: bool = True
|
|
140
|
+
heuristic_threshold: float = 0.7
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class LoopBreakerConfig:
|
|
145
|
+
"""Loop breaker settings."""
|
|
146
|
+
|
|
147
|
+
enabled: bool = True
|
|
148
|
+
window_size: int = 10
|
|
149
|
+
similarity_threshold: float = 0.85
|
|
150
|
+
max_consecutive: int = 3
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class AgentSecurityConfig:
|
|
155
|
+
"""Agent-security domain."""
|
|
156
|
+
|
|
157
|
+
enabled: bool = True
|
|
158
|
+
proxy: ProxyConfig = field(default_factory=ProxyConfig)
|
|
159
|
+
firewall: FirewallConfig = field(default_factory=FirewallConfig)
|
|
160
|
+
loop_breaker: LoopBreakerConfig = field(default_factory=LoopBreakerConfig)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class ForensicConfig:
|
|
165
|
+
"""Forensic black-box settings."""
|
|
166
|
+
|
|
167
|
+
storage: str = "minio"
|
|
168
|
+
bucket: str = "forensic-blackbox"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class OTELConfig:
|
|
173
|
+
"""OpenTelemetry settings."""
|
|
174
|
+
|
|
175
|
+
endpoint: str = "http://localhost:4317"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class ComplianceConfig:
|
|
180
|
+
"""Compliance domain."""
|
|
181
|
+
|
|
182
|
+
enabled: bool = True
|
|
183
|
+
forensic: ForensicConfig = field(default_factory=ForensicConfig)
|
|
184
|
+
eu_ai_act_enabled: bool = True
|
|
185
|
+
otel: OTELConfig = field(default_factory=OTELConfig)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class DashboardConfig:
|
|
190
|
+
"""Dashboard settings."""
|
|
191
|
+
|
|
192
|
+
enabled: bool = True
|
|
193
|
+
port: int = 3000
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class AlertChannelConfig:
|
|
198
|
+
"""A single alert channel."""
|
|
199
|
+
|
|
200
|
+
type: str = "log"
|
|
201
|
+
url: str = ""
|
|
202
|
+
events: list[str] = field(default_factory=list)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class AdminaConfig:
|
|
207
|
+
"""Top-level Admina configuration.
|
|
208
|
+
|
|
209
|
+
Constructed by :func:`load_config` — reads ``admina.yaml`` if present,
|
|
210
|
+
otherwise falls back to ``.env`` variables.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
version: str = "2.0"
|
|
214
|
+
data_sovereignty: DataSovereigntyConfig = field(default_factory=DataSovereigntyConfig)
|
|
215
|
+
ai_infra: AIInfraConfig = field(default_factory=AIInfraConfig)
|
|
216
|
+
agent_security: AgentSecurityConfig = field(default_factory=AgentSecurityConfig)
|
|
217
|
+
compliance: ComplianceConfig = field(default_factory=ComplianceConfig)
|
|
218
|
+
dashboard: DashboardConfig = field(default_factory=DashboardConfig)
|
|
219
|
+
forensic_store: str = "minio"
|
|
220
|
+
auth_provider: str = "apikey"
|
|
221
|
+
pii_engine: str = "spacy-regex"
|
|
222
|
+
alert_channels: list[AlertChannelConfig] = field(default_factory=list)
|
|
223
|
+
plugins: list[str] = field(default_factory=list)
|
|
224
|
+
|
|
225
|
+
# Storage — populated from .env fallback when YAML absent
|
|
226
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
227
|
+
clickhouse_host: str = "localhost"
|
|
228
|
+
clickhouse_port: int = 8123
|
|
229
|
+
clickhouse_db: str = "admina"
|
|
230
|
+
clickhouse_password: str = ""
|
|
231
|
+
minio_endpoint: str = "localhost:9000"
|
|
232
|
+
minio_access_key: str = "admina"
|
|
233
|
+
minio_secret_key: str = ""
|
|
234
|
+
minio_bucket: str = "forensic-blackbox"
|
|
235
|
+
minio_secure: bool = False
|
|
236
|
+
|
|
237
|
+
# Auth / rate-limit — populated from .env fallback
|
|
238
|
+
admina_api_key: str = ""
|
|
239
|
+
rate_limit_max_requests: int = 100
|
|
240
|
+
rate_limit_window_seconds: int = 60
|
|
241
|
+
log_level: str = "INFO"
|
|
242
|
+
cors_origins: str = "http://localhost:3000"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── YAML parsing helpers ─────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
|
|
249
|
+
"""Build an :class:`AdminaConfig` from a parsed YAML dict."""
|
|
250
|
+
domains = data.get("domains", {})
|
|
251
|
+
|
|
252
|
+
# data_sovereignty
|
|
253
|
+
ds_raw = domains.get("data_sovereignty", {})
|
|
254
|
+
pii_raw = ds_raw.get("pii", {})
|
|
255
|
+
res_raw = ds_raw.get("residency", {})
|
|
256
|
+
ds = DataSovereigntyConfig(
|
|
257
|
+
enabled=ds_raw.get("enabled", True),
|
|
258
|
+
pii=PIIConfig(
|
|
259
|
+
enabled=pii_raw.get("enabled", True),
|
|
260
|
+
categories=pii_raw.get(
|
|
261
|
+
"categories",
|
|
262
|
+
[
|
|
263
|
+
"email",
|
|
264
|
+
"phone",
|
|
265
|
+
"credit_card",
|
|
266
|
+
"ssn",
|
|
267
|
+
"iban",
|
|
268
|
+
"ip",
|
|
269
|
+
"person",
|
|
270
|
+
"org",
|
|
271
|
+
],
|
|
272
|
+
),
|
|
273
|
+
ner_model=pii_raw.get("ner_model", "en_core_web_sm"),
|
|
274
|
+
),
|
|
275
|
+
residency=ResidencyConfig(
|
|
276
|
+
enabled=res_raw.get("enabled", True),
|
|
277
|
+
allowed_zones=res_raw.get("allowed_zones", ["local", "eu"]),
|
|
278
|
+
block_outbound=res_raw.get("block_outbound", True),
|
|
279
|
+
),
|
|
280
|
+
classification_enabled=ds_raw.get("classification", {}).get("enabled", True),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# ai_infra
|
|
284
|
+
ai_raw = domains.get("ai_infra", {})
|
|
285
|
+
llm_raw = ai_raw.get("llm", {})
|
|
286
|
+
rag_raw = ai_raw.get("rag", {})
|
|
287
|
+
ai = AIInfraConfig(
|
|
288
|
+
enabled=ai_raw.get("enabled", False),
|
|
289
|
+
llm=LLMConfig(
|
|
290
|
+
enabled=llm_raw.get("enabled", True),
|
|
291
|
+
backend=llm_raw.get("backend", "ollama"),
|
|
292
|
+
model=llm_raw.get("model", "llama3.1:8b"),
|
|
293
|
+
gpu_autodetect=llm_raw.get("gpu_autodetect", True),
|
|
294
|
+
vram_limit_mb=llm_raw.get("vram_limit_mb", 0),
|
|
295
|
+
),
|
|
296
|
+
rag=RAGConfig(
|
|
297
|
+
enabled=rag_raw.get("enabled", True),
|
|
298
|
+
backend=rag_raw.get("backend", "chromadb"),
|
|
299
|
+
chunk_size=rag_raw.get("chunk_size", 512),
|
|
300
|
+
chunk_overlap=rag_raw.get("chunk_overlap", 50),
|
|
301
|
+
embedding_backend=rag_raw.get("embedding_backend", "ollama"),
|
|
302
|
+
embedding_model=rag_raw.get("embedding_model", "nomic-embed-text"),
|
|
303
|
+
),
|
|
304
|
+
webui=WebUIConfig(
|
|
305
|
+
enabled=ai_raw.get("webui", {}).get("enabled", True),
|
|
306
|
+
port=ai_raw.get("webui", {}).get("port", 3080),
|
|
307
|
+
auth_mode=ai_raw.get("webui", {}).get("auth_mode", "builtin"),
|
|
308
|
+
signup_enabled=ai_raw.get("webui", {}).get("signup_enabled", True),
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# agent_security
|
|
313
|
+
as_raw = domains.get("agent_security", {})
|
|
314
|
+
px_raw = as_raw.get("proxy", {})
|
|
315
|
+
fw_raw = as_raw.get("firewall", {})
|
|
316
|
+
lb_raw = as_raw.get("loop_breaker", {})
|
|
317
|
+
agent_sec = AgentSecurityConfig(
|
|
318
|
+
enabled=as_raw.get("enabled", True),
|
|
319
|
+
proxy=ProxyConfig(
|
|
320
|
+
port=px_raw.get("port", 8080),
|
|
321
|
+
upstream=px_raw.get("upstream", "http://localhost:9000"),
|
|
322
|
+
),
|
|
323
|
+
firewall=FirewallConfig(
|
|
324
|
+
enabled=fw_raw.get("enabled", True),
|
|
325
|
+
heuristic_threshold=fw_raw.get("heuristic_threshold", 0.7),
|
|
326
|
+
),
|
|
327
|
+
loop_breaker=LoopBreakerConfig(
|
|
328
|
+
enabled=lb_raw.get("enabled", True),
|
|
329
|
+
window_size=lb_raw.get("window_size", 10),
|
|
330
|
+
similarity_threshold=lb_raw.get("similarity_threshold", 0.85),
|
|
331
|
+
max_consecutive=lb_raw.get("max_consecutive", 3),
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# compliance
|
|
336
|
+
co_raw = domains.get("compliance", {})
|
|
337
|
+
fo_raw = co_raw.get("forensic", {})
|
|
338
|
+
ot_raw = co_raw.get("otel", {})
|
|
339
|
+
comp = ComplianceConfig(
|
|
340
|
+
enabled=co_raw.get("enabled", True),
|
|
341
|
+
forensic=ForensicConfig(
|
|
342
|
+
storage=fo_raw.get("storage", "minio"),
|
|
343
|
+
bucket=fo_raw.get("bucket", "forensic-blackbox"),
|
|
344
|
+
),
|
|
345
|
+
eu_ai_act_enabled=co_raw.get("eu_ai_act", {}).get("enabled", True),
|
|
346
|
+
otel=OTELConfig(endpoint=ot_raw.get("endpoint", "http://localhost:4317")),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# dashboard
|
|
350
|
+
dash_raw = data.get("dashboard", {})
|
|
351
|
+
dash = DashboardConfig(
|
|
352
|
+
enabled=dash_raw.get("enabled", True),
|
|
353
|
+
port=dash_raw.get("port", 3000),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# alert channels
|
|
357
|
+
alerts = [
|
|
358
|
+
AlertChannelConfig(
|
|
359
|
+
type=a.get("type", "log"),
|
|
360
|
+
url=a.get("url", ""),
|
|
361
|
+
events=a.get("events", []),
|
|
362
|
+
)
|
|
363
|
+
for a in data.get("alert_channels", [])
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
return AdminaConfig(
|
|
367
|
+
version=data.get("version", "2.0"),
|
|
368
|
+
data_sovereignty=ds,
|
|
369
|
+
ai_infra=ai,
|
|
370
|
+
agent_security=agent_sec,
|
|
371
|
+
compliance=comp,
|
|
372
|
+
dashboard=dash,
|
|
373
|
+
forensic_store=data.get("forensic_store", "minio"),
|
|
374
|
+
auth_provider=data.get("auth_provider", "apikey"),
|
|
375
|
+
pii_engine=data.get("pii_engine", "spacy-regex"),
|
|
376
|
+
alert_channels=alerts,
|
|
377
|
+
plugins=data.get("plugins", []),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _build_from_env() -> AdminaConfig:
|
|
382
|
+
"""Build an :class:`AdminaConfig` from environment / ``.env`` variables.
|
|
383
|
+
|
|
384
|
+
Maps the flat ``UPPERCASE`` env vars to the structured config object
|
|
385
|
+
so the rest of the codebase can use a single interface.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
def _env(key: str, default: str = "") -> str:
|
|
389
|
+
return os.environ.get(key, default)
|
|
390
|
+
|
|
391
|
+
def _env_int(key: str, default: int) -> int:
|
|
392
|
+
raw = os.environ.get(key)
|
|
393
|
+
if raw is None:
|
|
394
|
+
return default
|
|
395
|
+
try:
|
|
396
|
+
return int(raw)
|
|
397
|
+
except ValueError:
|
|
398
|
+
return default
|
|
399
|
+
|
|
400
|
+
def _env_float(key: str, default: float) -> float:
|
|
401
|
+
raw = os.environ.get(key)
|
|
402
|
+
if raw is None:
|
|
403
|
+
return default
|
|
404
|
+
try:
|
|
405
|
+
return float(raw)
|
|
406
|
+
except ValueError:
|
|
407
|
+
return default
|
|
408
|
+
|
|
409
|
+
def _env_bool(key: str, default: bool) -> bool:
|
|
410
|
+
raw = os.environ.get(key)
|
|
411
|
+
if raw is None:
|
|
412
|
+
return default
|
|
413
|
+
return raw.lower() in ("1", "true", "yes")
|
|
414
|
+
|
|
415
|
+
return AdminaConfig(
|
|
416
|
+
agent_security=AgentSecurityConfig(
|
|
417
|
+
proxy=ProxyConfig(
|
|
418
|
+
upstream=_env("UPSTREAM_MCP_URL", "http://localhost:9000"),
|
|
419
|
+
),
|
|
420
|
+
firewall=FirewallConfig(
|
|
421
|
+
enabled=_env_bool("INJECTION_FAST_PATH_ENABLED", True),
|
|
422
|
+
),
|
|
423
|
+
loop_breaker=LoopBreakerConfig(
|
|
424
|
+
window_size=_env_int("LOOP_WINDOW_SIZE", 10),
|
|
425
|
+
similarity_threshold=_env_float("LOOP_SIMILARITY_THRESHOLD", 0.85),
|
|
426
|
+
max_consecutive=_env_int("LOOP_MAX_CONSECUTIVE", 3),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
compliance=ComplianceConfig(
|
|
430
|
+
otel=OTELConfig(endpoint=_env("OTEL_ENDPOINT", "http://localhost:4317")),
|
|
431
|
+
forensic=ForensicConfig(
|
|
432
|
+
bucket=_env("MINIO_BUCKET", "forensic-blackbox"),
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
redis_url=_env("REDIS_URL", "redis://localhost:6379/0"),
|
|
436
|
+
clickhouse_host=_env("CLICKHOUSE_HOST", "localhost"),
|
|
437
|
+
clickhouse_port=_env_int("CLICKHOUSE_PORT", 8123),
|
|
438
|
+
clickhouse_db=_env("CLICKHOUSE_DB", "admina"),
|
|
439
|
+
clickhouse_password=_env("CLICKHOUSE_PASSWORD", ""),
|
|
440
|
+
minio_endpoint=_env("MINIO_ENDPOINT", "localhost:9000"),
|
|
441
|
+
minio_access_key=_env("MINIO_ACCESS_KEY", "admina"),
|
|
442
|
+
minio_secret_key=_env("MINIO_SECRET_KEY", ""),
|
|
443
|
+
minio_bucket=_env("MINIO_BUCKET", "forensic-blackbox"),
|
|
444
|
+
minio_secure=_env_bool("MINIO_SECURE", False),
|
|
445
|
+
admina_api_key=_env("ADMINA_API_KEY", ""),
|
|
446
|
+
rate_limit_max_requests=_env_int("RATE_LIMIT_MAX_REQUESTS", 100),
|
|
447
|
+
rate_limit_window_seconds=_env_int("RATE_LIMIT_WINDOW_SECONDS", 60),
|
|
448
|
+
log_level=_env("LOG_LEVEL", "INFO"),
|
|
449
|
+
cors_origins=_env("CORS_ORIGINS", "http://localhost:3000"),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ── Public API ───────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def load_config(
|
|
457
|
+
yaml_path: str | Path | None = None,
|
|
458
|
+
*,
|
|
459
|
+
search_paths: list[str | Path] | None = None,
|
|
460
|
+
) -> AdminaConfig:
|
|
461
|
+
"""Load configuration from ``admina.yaml`` or ``.env`` fallback.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
yaml_path: Explicit path to a YAML config file.
|
|
465
|
+
search_paths: Directories to search for ``admina.yaml`` when
|
|
466
|
+
*yaml_path* is not given. Defaults to cwd and repo root.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
A fully populated :class:`AdminaConfig` instance.
|
|
470
|
+
"""
|
|
471
|
+
# 1. Explicit path
|
|
472
|
+
if yaml_path is not None:
|
|
473
|
+
path = Path(yaml_path)
|
|
474
|
+
if path.is_file() and _HAS_YAML:
|
|
475
|
+
return _load_yaml(path)
|
|
476
|
+
|
|
477
|
+
# 2. Search common locations
|
|
478
|
+
if search_paths is None:
|
|
479
|
+
search_paths = [Path.cwd(), Path(__file__).resolve().parent.parent]
|
|
480
|
+
for base in search_paths:
|
|
481
|
+
candidate = Path(base) / "admina.yaml"
|
|
482
|
+
if candidate.is_file() and _HAS_YAML:
|
|
483
|
+
return _load_yaml(candidate)
|
|
484
|
+
|
|
485
|
+
# 3. Fallback to environment / .env
|
|
486
|
+
logger.info("No admina.yaml found — using .env fallback")
|
|
487
|
+
return _build_from_env()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _load_yaml(path: Path) -> AdminaConfig:
|
|
491
|
+
"""Parse a YAML file and return :class:`AdminaConfig`."""
|
|
492
|
+
logger.info("Loading config from %s", path)
|
|
493
|
+
with open(path) as fh:
|
|
494
|
+
data = yaml.safe_load(fh) or {}
|
|
495
|
+
if not isinstance(data, dict):
|
|
496
|
+
raise ValueError(f"admina.yaml must be a YAML mapping, got {type(data).__name__}")
|
|
497
|
+
return _build_from_yaml(data)
|
admina/core/event_bus.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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 event bus for governance events.
|
|
16
|
+
|
|
17
|
+
Provides a pub/sub mechanism for governance events across the framework.
|
|
18
|
+
Subscribers are wired at proxy startup and include:
|
|
19
|
+
- OTEL exporter (governance decision spans)
|
|
20
|
+
- Alert channels (BLOCK/CIRCUIT_BREAK notifications)
|
|
21
|
+
- Dashboard WebSocket (live event feed to connected clients)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import UTC, datetime
|
|
30
|
+
|
|
31
|
+
from admina.core.types import EventType
|
|
32
|
+
|
|
33
|
+
__all__ = ["EventBus", "EventType", "GovernanceEvent", "bus"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class GovernanceEvent:
|
|
38
|
+
"""A single governance event emitted by the framework.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
event_type: The type of governance event.
|
|
42
|
+
timestamp: When the event occurred.
|
|
43
|
+
session_id: Optional session identifier.
|
|
44
|
+
user_id: Optional user identifier.
|
|
45
|
+
domain: Optional domain name (e.g. data-sovereignty, ai-infra).
|
|
46
|
+
action: Optional action taken (ALLOW, BLOCK, REDACT, CIRCUIT_BREAK).
|
|
47
|
+
risk_level: Optional risk level assessment.
|
|
48
|
+
metadata: Additional event-specific data.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
event_type: EventType
|
|
52
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
53
|
+
session_id: str | None = None
|
|
54
|
+
user_id: str | None = None
|
|
55
|
+
domain: str | None = None
|
|
56
|
+
action: str | None = None
|
|
57
|
+
risk_level: str | None = None
|
|
58
|
+
metadata: dict = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EventBus:
|
|
62
|
+
"""Async pub/sub event bus for governance events.
|
|
63
|
+
|
|
64
|
+
Supports per-type subscriptions and wildcard subscribers that
|
|
65
|
+
receive all events.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
"""Initialize the event bus with empty subscriber lists."""
|
|
70
|
+
self._subscribers: dict[EventType, list[Callable]] = {}
|
|
71
|
+
self._wildcard_subscribers: list[Callable] = []
|
|
72
|
+
|
|
73
|
+
def subscribe(self, event_type: EventType, callback: Callable) -> None:
|
|
74
|
+
"""Subscribe to a specific event type.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
event_type: The event type to listen for.
|
|
78
|
+
callback: Callable invoked with the GovernanceEvent.
|
|
79
|
+
Can be sync or async.
|
|
80
|
+
"""
|
|
81
|
+
if event_type not in self._subscribers:
|
|
82
|
+
self._subscribers[event_type] = []
|
|
83
|
+
self._subscribers[event_type].append(callback)
|
|
84
|
+
|
|
85
|
+
def subscribe_all(self, callback: Callable) -> None:
|
|
86
|
+
"""Subscribe to all event types.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
callback: Callable invoked with every GovernanceEvent.
|
|
90
|
+
Can be sync or async.
|
|
91
|
+
"""
|
|
92
|
+
self._wildcard_subscribers.append(callback)
|
|
93
|
+
|
|
94
|
+
async def emit(self, event: GovernanceEvent) -> None:
|
|
95
|
+
"""Emit an event to all matching subscribers.
|
|
96
|
+
|
|
97
|
+
Calls per-type subscribers first, then wildcard subscribers.
|
|
98
|
+
Both sync and async callbacks are supported.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
event: The governance event to emit.
|
|
102
|
+
"""
|
|
103
|
+
callbacks = list(self._subscribers.get(event.event_type, []))
|
|
104
|
+
callbacks.extend(self._wildcard_subscribers)
|
|
105
|
+
|
|
106
|
+
for callback in callbacks:
|
|
107
|
+
result = callback(event)
|
|
108
|
+
if asyncio.iscoroutine(result):
|
|
109
|
+
await result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
bus = EventBus()
|