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,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
+ )