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,341 @@
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 callback handler for LangChain.
16
+
17
+ Provides :class:`AdminaCallbackHandler` — a LangChain ``BaseCallbackHandler``
18
+ that validates every LLM call, tool invocation, and chain output through
19
+ the Admina governance pipeline (firewall, PII redaction, loop detection).
20
+
21
+ Works **in-process** via the Admina SDK — no sidecar proxy needed.
22
+
23
+ Usage::
24
+
25
+ from langchain_openai import ChatOpenAI
26
+ from admina.integrations.langchain.callbacks import AdminaCallbackHandler
27
+
28
+ handler = AdminaCallbackHandler()
29
+ llm = ChatOpenAI(callbacks=[handler])
30
+ llm.invoke("Summarize this document")
31
+
32
+ # Check governance results
33
+ print(handler.last_result)
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import logging
39
+ import time
40
+ import uuid
41
+ from dataclasses import dataclass, field
42
+ from typing import Any
43
+
44
+ from admina.core.event_bus import GovernanceEvent, bus
45
+ from admina.core.types import EventType
46
+ from admina.integrations._engines import get_firewall, get_loop_breaker, get_pii_redactor
47
+ from admina.sdk._compat import run_sync
48
+
49
+ logger = logging.getLogger("admina.integrations.langchain")
50
+
51
+
52
+ class GovernanceBlockedError(Exception):
53
+ """Raised when Admina blocks a request due to governance policy."""
54
+
55
+ def __init__(self, action: str, risk_level: str, details: dict | None = None):
56
+ self.action = action
57
+ self.risk_level = risk_level
58
+ self.details = details or {}
59
+ super().__init__(f"Admina governance {action}: risk_level={risk_level}")
60
+
61
+
62
+ @dataclass
63
+ class GovernanceResult:
64
+ """Result of an Admina governance check."""
65
+
66
+ action: str = "ALLOW"
67
+ risk_level: str = "LOW"
68
+ pii_count: int = 0
69
+ redacted_text: str | None = None
70
+ checks: dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ class AdminaCallbackHandler:
74
+ """LangChain callback handler with Admina governance.
75
+
76
+ Intercepts LLM calls, tool invocations, and chain runs to
77
+ enforce PII redaction, injection firewall, and loop detection.
78
+
79
+ Args:
80
+ session_id: Session identifier for loop detection across calls.
81
+ pii_redaction: Whether to redact PII from prompts/responses.
82
+ firewall: Whether to check for prompt injections.
83
+ loop_detection: Whether to detect reasoning loops.
84
+ on_block: Action when governance blocks a request:
85
+ ``"raise"`` raises :class:`GovernanceBlockedError`,
86
+ ``"warn"`` logs a warning and continues.
87
+ audit: Whether to emit events to the governance event bus.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ session_id: str | None = None,
93
+ pii_redaction: bool = True,
94
+ firewall: bool = True,
95
+ loop_detection: bool = True,
96
+ on_block: str = "raise",
97
+ audit: bool = True,
98
+ ) -> None:
99
+ self.session_id = session_id or f"langchain-{uuid.uuid4().hex[:8]}"
100
+ self.pii_redaction = pii_redaction
101
+ self.firewall = firewall
102
+ self.loop_detection = loop_detection
103
+ self.on_block = on_block
104
+ self.audit = audit
105
+
106
+ self.last_result: GovernanceResult | None = None
107
+ self._call_count = 0
108
+ self._block_count = 0
109
+ self._redact_count = 0
110
+
111
+ # ── Internal governance check ────────────────────────────
112
+
113
+ def _govern(self, text: str, direction: str = "inbound") -> GovernanceResult:
114
+ """Run governance checks on text and return result."""
115
+ result = GovernanceResult()
116
+ start = time.perf_counter()
117
+
118
+ # Firewall
119
+ if self.firewall and direction == "inbound":
120
+ fw = get_firewall()
121
+ fw_result = fw.check(text)
122
+ result.checks["firewall"] = fw_result
123
+ if fw_result.get("is_injection"):
124
+ result.action = "BLOCK"
125
+ rl = fw_result.get("risk_level", "HIGH")
126
+ result.risk_level = rl.value if hasattr(rl, "value") else str(rl)
127
+
128
+ # Loop detection
129
+ if self.loop_detection and direction == "inbound":
130
+ lb = get_loop_breaker()
131
+ lb_result = lb.check(self.session_id, text)
132
+ result.checks["loop_breaker"] = lb_result
133
+ if lb_result.get("is_loop"):
134
+ result.action = "CIRCUIT_BREAK"
135
+ result.risk_level = "HIGH"
136
+
137
+ # PII redaction
138
+ if self.pii_redaction:
139
+ pii = get_pii_redactor()
140
+ pii_result = pii.redact(text)
141
+ result.checks["pii_redaction"] = {
142
+ "count": pii_result["count"],
143
+ "entities": [e["type"] for e in pii_result.get("entities", [])],
144
+ }
145
+ if pii_result["count"] > 0:
146
+ result.pii_count = pii_result["count"]
147
+ result.redacted_text = pii_result["redacted_text"]
148
+ if result.action == "ALLOW":
149
+ result.action = "REDACT"
150
+
151
+ result.checks["latency_ms"] = round((time.perf_counter() - start) * 1000, 2)
152
+ self.last_result = result
153
+ return result
154
+
155
+ def _handle_block(self, result: GovernanceResult, context: str) -> None:
156
+ """Handle a BLOCK or CIRCUIT_BREAK governance decision."""
157
+ self._block_count += 1
158
+ if self.on_block == "raise":
159
+ raise GovernanceBlockedError(result.action, result.risk_level, result.checks)
160
+ logger.warning(
161
+ "[BLOCKED] %s: action=%s risk=%s",
162
+ context,
163
+ result.action,
164
+ result.risk_level,
165
+ )
166
+
167
+ def _emit(self, event_type: EventType, **kwargs: Any) -> None:
168
+ """Emit a governance event to the bus."""
169
+ if not self.audit:
170
+ return
171
+ event = GovernanceEvent(
172
+ event_type=event_type,
173
+ session_id=self.session_id,
174
+ domain="langchain",
175
+ **kwargs,
176
+ )
177
+ run_sync(bus.emit(event))
178
+
179
+ # ── LangChain callback interface ─────────────────────────
180
+ # These methods match LangChain's BaseCallbackHandler protocol.
181
+ # We don't inherit from it to avoid requiring langchain as a
182
+ # dependency — LangChain checks for method presence via duck typing.
183
+
184
+ def on_llm_start(
185
+ self,
186
+ serialized: dict[str, Any],
187
+ prompts: list[str],
188
+ *,
189
+ run_id: Any = None,
190
+ **kwargs: Any,
191
+ ) -> None:
192
+ """Called when an LLM starts generating."""
193
+ self._call_count += 1
194
+ combined = "\n".join(prompts)
195
+
196
+ result = self._govern(combined, direction="inbound")
197
+
198
+ self._emit(
199
+ EventType.MODEL_CALL,
200
+ action=result.action,
201
+ risk_level=result.risk_level,
202
+ metadata={
203
+ "model": serialized.get("name", "unknown"),
204
+ "prompt_length": len(combined),
205
+ "pii_count": result.pii_count,
206
+ },
207
+ )
208
+
209
+ if result.action in ("BLOCK", "CIRCUIT_BREAK"):
210
+ self._handle_block(result, "on_llm_start")
211
+
212
+ def on_llm_end(
213
+ self,
214
+ response: Any,
215
+ *,
216
+ run_id: Any = None,
217
+ **kwargs: Any,
218
+ ) -> None:
219
+ """Called when an LLM finishes generating."""
220
+ # Extract text from LLMResult
221
+ text = ""
222
+ if hasattr(response, "generations") and response.generations:
223
+ gen = response.generations[0]
224
+ if gen:
225
+ text = gen[0].text if hasattr(gen[0], "text") else str(gen[0])
226
+
227
+ if text:
228
+ result = self._govern(text, direction="outbound")
229
+ if result.pii_count > 0:
230
+ self._redact_count += 1
231
+
232
+ self._emit(
233
+ EventType.MODEL_RESPONSE,
234
+ action=result.action,
235
+ risk_level=result.risk_level,
236
+ metadata={
237
+ "response_length": len(text),
238
+ "pii_redacted": result.pii_count,
239
+ },
240
+ )
241
+
242
+ def on_llm_error(
243
+ self,
244
+ error: BaseException,
245
+ *,
246
+ run_id: Any = None,
247
+ **kwargs: Any,
248
+ ) -> None:
249
+ """Called when an LLM errors."""
250
+ logger.warning("LLM error in governed session %s: %s", self.session_id, error)
251
+
252
+ def on_tool_start(
253
+ self,
254
+ serialized: dict[str, Any],
255
+ input_str: str,
256
+ *,
257
+ run_id: Any = None,
258
+ **kwargs: Any,
259
+ ) -> None:
260
+ """Called when a tool starts running."""
261
+ result = self._govern(input_str, direction="inbound")
262
+
263
+ self._emit(
264
+ EventType.AGENT_REQUEST,
265
+ action=result.action,
266
+ risk_level=result.risk_level,
267
+ metadata={
268
+ "tool": serialized.get("name", "unknown"),
269
+ "input_length": len(input_str),
270
+ },
271
+ )
272
+
273
+ if result.action in ("BLOCK", "CIRCUIT_BREAK"):
274
+ self._handle_block(result, f"on_tool_start({serialized.get('name', '?')})")
275
+
276
+ def on_tool_end(
277
+ self,
278
+ output: str,
279
+ *,
280
+ run_id: Any = None,
281
+ **kwargs: Any,
282
+ ) -> None:
283
+ """Called when a tool finishes."""
284
+ if output:
285
+ result = self._govern(str(output), direction="outbound")
286
+ if result.pii_count > 0:
287
+ self._redact_count += 1
288
+
289
+ self._emit(
290
+ EventType.AGENT_RESPONSE,
291
+ action=result.action,
292
+ risk_level=result.risk_level,
293
+ metadata={"output_length": len(str(output)), "pii_redacted": result.pii_count},
294
+ )
295
+
296
+ def on_chain_start(
297
+ self,
298
+ serialized: dict[str, Any],
299
+ inputs: dict[str, Any],
300
+ *,
301
+ run_id: Any = None,
302
+ **kwargs: Any,
303
+ ) -> None:
304
+ """Called when a chain starts."""
305
+ pass # governance is applied at LLM/tool level
306
+
307
+ def on_chain_end(
308
+ self,
309
+ outputs: dict[str, Any],
310
+ *,
311
+ run_id: Any = None,
312
+ **kwargs: Any,
313
+ ) -> None:
314
+ """Called when a chain finishes."""
315
+ pass
316
+
317
+ def on_chain_error(
318
+ self,
319
+ error: BaseException,
320
+ *,
321
+ run_id: Any = None,
322
+ **kwargs: Any,
323
+ ) -> None:
324
+ """Called when a chain errors."""
325
+ pass
326
+
327
+ # ── Stats ────────────────────────────────────────────────
328
+
329
+ def get_stats(self) -> dict[str, Any]:
330
+ """Return governance statistics for this handler."""
331
+ return {
332
+ "session_id": self.session_id,
333
+ "call_count": self._call_count,
334
+ "block_count": self._block_count,
335
+ "redact_count": self._redact_count,
336
+ "features": {
337
+ "firewall": self.firewall,
338
+ "pii_redaction": self.pii_redaction,
339
+ "loop_detection": self.loop_detection,
340
+ },
341
+ }
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,49 @@
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 plugin system.
16
+
17
+ Provides abstract base classes for all 9 plugin interfaces.
18
+ Community developers extend these to add new model adapters,
19
+ data connectors, governance guards, and more.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from admina.plugins.base import (
25
+ BaseAlertChannel,
26
+ BaseAuthProvider,
27
+ BaseComplianceTemplate,
28
+ BaseDataConnector,
29
+ BaseForensicStore,
30
+ BaseGovernanceGuard,
31
+ BaseModelAdapter,
32
+ BasePIIEngine,
33
+ BaseTransportAdapter,
34
+ )
35
+ from admina.plugins.registry import PLUGIN_TYPES, PluginRegistry
36
+
37
+ __all__ = [
38
+ "BaseModelAdapter",
39
+ "BaseDataConnector",
40
+ "BaseGovernanceGuard",
41
+ "BaseComplianceTemplate",
42
+ "BaseTransportAdapter",
43
+ "BaseForensicStore",
44
+ "BaseAuthProvider",
45
+ "BasePIIEngine",
46
+ "BaseAlertChannel",
47
+ "PluginRegistry",
48
+ "PLUGIN_TYPES",
49
+ ]