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
|
@@ -0,0 +1,232 @@
|
|
|
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 — Governance pipeline.
|
|
16
|
+
|
|
17
|
+
Orchestrates all governance checks (loop breaker, firewall, PII redaction,
|
|
18
|
+
pluggable guards) in sequence and returns a GovernanceResult.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from admina.core.types import GovernanceAction, RiskLevel
|
|
30
|
+
from admina.core.types import GovernanceResponse as GovResponse
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("admina.proxy")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GovernanceResult:
|
|
37
|
+
"""Result of running the full governance pipeline on a request."""
|
|
38
|
+
|
|
39
|
+
action: GovernanceAction = GovernanceAction.ALLOW
|
|
40
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
41
|
+
checks: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
redacted_body: dict | None = None
|
|
43
|
+
gov_response: GovResponse | None = None
|
|
44
|
+
latency_ms: float = 0.0
|
|
45
|
+
# Set in observe / dry-run mode: the action that *would* have been taken
|
|
46
|
+
# in enforce mode. Useful for dashboards and policy tuning.
|
|
47
|
+
would_action: GovernanceAction | None = None
|
|
48
|
+
mode: str = "enforce"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def run_pipeline(
|
|
52
|
+
*,
|
|
53
|
+
body: dict,
|
|
54
|
+
content_str: str,
|
|
55
|
+
session_id: str,
|
|
56
|
+
agent_id: str,
|
|
57
|
+
request_id: str,
|
|
58
|
+
params: dict,
|
|
59
|
+
firewall: Any,
|
|
60
|
+
pii_redactor: Any,
|
|
61
|
+
loop_breaker: Any,
|
|
62
|
+
governance_guards: list,
|
|
63
|
+
injection_enabled: bool = True,
|
|
64
|
+
pii_enabled: bool = True,
|
|
65
|
+
mode: str = "enforce",
|
|
66
|
+
) -> GovernanceResult:
|
|
67
|
+
"""Execute the full governance pipeline and return a GovernanceResult.
|
|
68
|
+
|
|
69
|
+
This function is pure logic — no HTTP, no storage, no side effects.
|
|
70
|
+
The caller (mcp_proxy) handles rate limiting, forensic storage,
|
|
71
|
+
ClickHouse, event bus, and HTTP responses.
|
|
72
|
+
"""
|
|
73
|
+
start_time = time.perf_counter()
|
|
74
|
+
result = GovernanceResult()
|
|
75
|
+
result.redacted_body = body
|
|
76
|
+
result.mode = mode
|
|
77
|
+
|
|
78
|
+
# 1. Loop Breaker
|
|
79
|
+
loop_result = loop_breaker.check(session_id, content_str)
|
|
80
|
+
result.checks["loop_breaker"] = loop_result
|
|
81
|
+
if loop_result["is_loop"]:
|
|
82
|
+
result.action = GovernanceAction.CIRCUIT_BREAK
|
|
83
|
+
result.risk_level = RiskLevel.HIGH
|
|
84
|
+
|
|
85
|
+
# 2. Anti-Injection Firewall
|
|
86
|
+
if result.action != GovernanceAction.CIRCUIT_BREAK and injection_enabled:
|
|
87
|
+
texts_to_scan = _extract_text_fields(body)
|
|
88
|
+
for text in texts_to_scan:
|
|
89
|
+
fw_result = firewall.check(text)
|
|
90
|
+
result.checks["firewall"] = fw_result
|
|
91
|
+
if fw_result["is_injection"]:
|
|
92
|
+
result.action = GovernanceAction.BLOCK
|
|
93
|
+
result.risk_level = fw_result["risk_level"]
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
# 3. PII Redaction
|
|
97
|
+
pii_count = 0
|
|
98
|
+
if result.action == GovernanceAction.ALLOW and pii_enabled:
|
|
99
|
+
redacted_params, pii_result = _redact_params(params, pii_redactor)
|
|
100
|
+
result.checks["pii_redaction"] = pii_result
|
|
101
|
+
pii_count = pii_result["count"]
|
|
102
|
+
if pii_count > 0:
|
|
103
|
+
result.redacted_body = {**body, "params": redacted_params}
|
|
104
|
+
|
|
105
|
+
# 4. Pluggable Governance Guards
|
|
106
|
+
if result.action == GovernanceAction.ALLOW and governance_guards:
|
|
107
|
+
guard_payload = {"content": content_str, "params": params}
|
|
108
|
+
for guard in governance_guards:
|
|
109
|
+
try:
|
|
110
|
+
guard_result = await guard.inspect_request(guard_payload)
|
|
111
|
+
result.checks[f"guard_{guard.name}"] = guard_result
|
|
112
|
+
if guard_result.get("action") in ("BLOCK", "REDACT"):
|
|
113
|
+
result.action = GovernanceAction.BLOCK
|
|
114
|
+
result.risk_level = guard_result.get("risk_level", RiskLevel.HIGH)
|
|
115
|
+
break
|
|
116
|
+
except (ValueError, RuntimeError, OSError, TypeError) as exc:
|
|
117
|
+
logger.warning("Guard %r raised an exception: %s", guard.name, exc)
|
|
118
|
+
|
|
119
|
+
# 5. Apply governance MODE — observe / dry-run downgrade BLOCK to ALLOW
|
|
120
|
+
# but record what would have happened in `would_action` so dashboards
|
|
121
|
+
# and the suggestion engine still see the policy decision.
|
|
122
|
+
if mode in ("observe", "dry-run") and result.action in (
|
|
123
|
+
GovernanceAction.BLOCK,
|
|
124
|
+
GovernanceAction.CIRCUIT_BREAK,
|
|
125
|
+
):
|
|
126
|
+
result.would_action = result.action
|
|
127
|
+
logger.info(
|
|
128
|
+
"[%s] would have %s (risk=%s) — pass-through",
|
|
129
|
+
mode.upper(),
|
|
130
|
+
result.action.value,
|
|
131
|
+
result.risk_level,
|
|
132
|
+
)
|
|
133
|
+
result.action = GovernanceAction.ALLOW
|
|
134
|
+
|
|
135
|
+
# 6. Compute latency
|
|
136
|
+
result.latency_ms = (time.perf_counter() - start_time) * 1000
|
|
137
|
+
|
|
138
|
+
# 7. Build GovernanceResponse
|
|
139
|
+
result.gov_response = _build_gov_response(result, request_id, loop_result, pii_count)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --- helpers (moved from proxy/main.py) ---
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _extract_text_fields(obj: Any, depth: int = 0) -> list[str]:
|
|
148
|
+
"""Recursively extract all string fields from a dict/list."""
|
|
149
|
+
if depth > 5:
|
|
150
|
+
return []
|
|
151
|
+
texts: list[str] = []
|
|
152
|
+
if isinstance(obj, str):
|
|
153
|
+
texts.append(obj)
|
|
154
|
+
elif isinstance(obj, dict):
|
|
155
|
+
for v in obj.values():
|
|
156
|
+
texts.extend(_extract_text_fields(v, depth + 1))
|
|
157
|
+
elif isinstance(obj, list):
|
|
158
|
+
for item in obj:
|
|
159
|
+
texts.extend(_extract_text_fields(item, depth + 1))
|
|
160
|
+
return texts
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _redact_params(params: dict, pii_redactor: Any) -> tuple[dict, dict]:
|
|
164
|
+
"""Redact PII from all string values in params."""
|
|
165
|
+
total_result: dict[str, Any] = {"redacted_text": "", "entities": [], "count": 0}
|
|
166
|
+
redacted = _deep_redact(params, total_result, pii_redactor)
|
|
167
|
+
return redacted, total_result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _deep_redact(obj: Any, result: dict, pii_redactor: Any, depth: int = 0) -> Any:
|
|
171
|
+
if depth > 5:
|
|
172
|
+
return obj
|
|
173
|
+
if isinstance(obj, str):
|
|
174
|
+
r = pii_redactor.redact(obj)
|
|
175
|
+
result["entities"].extend(r["entities"])
|
|
176
|
+
result["count"] += r["count"]
|
|
177
|
+
return r["redacted_text"]
|
|
178
|
+
elif isinstance(obj, dict):
|
|
179
|
+
return {k: _deep_redact(v, result, pii_redactor, depth + 1) for k, v in obj.items()}
|
|
180
|
+
elif isinstance(obj, list):
|
|
181
|
+
return [_deep_redact(item, result, pii_redactor, depth + 1) for item in obj]
|
|
182
|
+
return obj
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def safe_serialize(obj: Any) -> Any:
|
|
186
|
+
"""Make object JSON-serializable."""
|
|
187
|
+
if hasattr(obj, "value"):
|
|
188
|
+
return obj.value
|
|
189
|
+
return obj
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _build_gov_response(
|
|
193
|
+
result: GovernanceResult,
|
|
194
|
+
request_id: str,
|
|
195
|
+
loop_result: dict,
|
|
196
|
+
pii_count: int,
|
|
197
|
+
) -> GovResponse:
|
|
198
|
+
"""Build a protocol-agnostic GovernanceResponse from pipeline results."""
|
|
199
|
+
_guard_block = next(
|
|
200
|
+
(
|
|
201
|
+
k
|
|
202
|
+
for k, v in result.checks.items()
|
|
203
|
+
if k.startswith("guard_") and v.get("action") in ("BLOCK", "REDACT")
|
|
204
|
+
),
|
|
205
|
+
None,
|
|
206
|
+
)
|
|
207
|
+
_deciding_domain = (
|
|
208
|
+
"loop_breaker"
|
|
209
|
+
if result.action == GovernanceAction.CIRCUIT_BREAK
|
|
210
|
+
else (
|
|
211
|
+
_guard_block.removeprefix("guard_")
|
|
212
|
+
if _guard_block and result.action == GovernanceAction.BLOCK
|
|
213
|
+
else (
|
|
214
|
+
"firewall"
|
|
215
|
+
if result.action == GovernanceAction.BLOCK
|
|
216
|
+
else (
|
|
217
|
+
"pii" if result.checks.get("pii_redaction", {}).get("count", 0) > 0 else "none"
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
_action_raw = result.action
|
|
223
|
+
_risk_raw = result.risk_level
|
|
224
|
+
return GovResponse(
|
|
225
|
+
content=json.dumps(result.redacted_body, default=str),
|
|
226
|
+
action=(_action_raw.value if hasattr(_action_raw, "value") else _action_raw).upper(),
|
|
227
|
+
risk_level=(_risk_raw.value if hasattr(_risk_raw, "value") else _risk_raw).upper(),
|
|
228
|
+
domain=_deciding_domain,
|
|
229
|
+
latency_us=result.latency_ms * 1000,
|
|
230
|
+
request_id=request_id,
|
|
231
|
+
metadata={"similarity": loop_result.get("similarity")},
|
|
232
|
+
)
|