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,391 @@
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 — GovernedAgent SDK primitive.
16
+
17
+ Exposes the existing proxy governance pipeline (firewall, loop breaker,
18
+ PII redaction) as a Python object for programmatic agent-to-agent calls.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import time
24
+ import uuid
25
+ from collections.abc import Awaitable, Callable
26
+ from dataclasses import dataclass, field
27
+ from typing import Any
28
+
29
+ from admina.core.event_bus import EventType, GovernanceEvent, bus
30
+ from admina.sdk._compat import run_sync
31
+
32
+ __all__ = ["GovernedAgent", "GovernedMCPResponse"]
33
+
34
+
35
+ @dataclass
36
+ class GovernedMCPResponse:
37
+ """Response from GovernedAgent.call().
38
+
39
+ Attributes:
40
+ result: The upstream response payload.
41
+ action: Governance action taken (ALLOW, BLOCK, REDACT, CIRCUIT_BREAK).
42
+ risk_level: Risk level of the request.
43
+ governance: Full governance details (domain results, latency).
44
+ """
45
+
46
+ result: Any = None
47
+ action: str = "ALLOW"
48
+ risk_level: str = "LOW"
49
+ governance: dict[str, Any] = field(default_factory=dict)
50
+
51
+
52
+ UpstreamCallable = Callable[..., Awaitable[dict]]
53
+
54
+
55
+ def _load_firewall() -> Any:
56
+ """Lazily load the InjectionFirewall."""
57
+ from admina.domains.agent_security.firewall import InjectionFirewall
58
+
59
+ return InjectionFirewall()
60
+
61
+
62
+ def _load_loop_breaker() -> Any:
63
+ """Lazily load the LoopBreaker."""
64
+ from admina.domains.agent_security.loop_breaker import LoopBreaker
65
+
66
+ return LoopBreaker()
67
+
68
+
69
+ def _load_pii_redactor() -> Any:
70
+ """Lazily load the PII redactor."""
71
+ from admina.domains.data_sovereignty.pii import PIIRedactor
72
+
73
+ return PIIRedactor()
74
+
75
+
76
+ def _extract_text(params: dict) -> str:
77
+ """Extract scannable text from params dict.
78
+
79
+ Args:
80
+ params: The call params.
81
+
82
+ Returns:
83
+ Concatenated string values for scanning.
84
+ """
85
+ texts: list[str] = []
86
+ _collect_strings(params, texts, depth=0)
87
+ return " ".join(texts) if texts else ""
88
+
89
+
90
+ def _collect_strings(obj: Any, acc: list[str], depth: int) -> None:
91
+ """Recursively collect string values from nested structures.
92
+
93
+ Args:
94
+ obj: Value to inspect.
95
+ acc: Accumulator list.
96
+ depth: Recursion depth (capped at 5).
97
+ """
98
+ if depth > 5:
99
+ return
100
+ if isinstance(obj, str):
101
+ acc.append(obj)
102
+ elif isinstance(obj, dict):
103
+ for v in obj.values():
104
+ _collect_strings(v, acc, depth + 1)
105
+ elif isinstance(obj, list):
106
+ for item in obj:
107
+ _collect_strings(item, acc, depth + 1)
108
+
109
+
110
+ def _redact_dict(obj: Any, pii_redactor: Any, depth: int = 0) -> tuple[Any, int]:
111
+ """Recursively redact PII in nested structures.
112
+
113
+ Args:
114
+ obj: Value to redact.
115
+ pii_redactor: Object with .redact(text) method.
116
+ depth: Recursion depth (capped at 5).
117
+
118
+ Returns:
119
+ Tuple of (redacted value, total PII count).
120
+ """
121
+ if depth > 5:
122
+ return obj, 0
123
+ if isinstance(obj, str):
124
+ r = pii_redactor.redact(obj)
125
+ return r["redacted_text"], r["count"]
126
+ if isinstance(obj, dict):
127
+ total = 0
128
+ out = {}
129
+ for k, v in obj.items():
130
+ rv, c = _redact_dict(v, pii_redactor, depth + 1)
131
+ out[k] = rv
132
+ total += c
133
+ return out, total
134
+ if isinstance(obj, list):
135
+ total = 0
136
+ out_list = []
137
+ for item in obj:
138
+ rv, c = _redact_dict(item, pii_redactor, depth + 1)
139
+ out_list.append(rv)
140
+ total += c
141
+ return out_list, total
142
+ return obj, 0
143
+
144
+
145
+ class GovernedAgent:
146
+ """SDK primitive for governed agent-to-agent calls.
147
+
148
+ Wraps an upstream callable with the full governance pipeline:
149
+ firewall check, loop detection, PII redaction (bidirectional),
150
+ and event emission.
151
+
152
+ Args:
153
+ upstream: An async callable that performs the actual upstream call,
154
+ accepting (method, params, **kwargs) and returning a dict.
155
+ audit: Whether to emit governance events.
156
+ pii_redaction: Whether to run PII redaction.
157
+ firewall_enabled: Whether to run the injection firewall.
158
+ loop_detection: Whether to run loop detection.
159
+ """
160
+
161
+ def __init__(
162
+ self,
163
+ upstream: UpstreamCallable,
164
+ audit: bool = True,
165
+ pii_redaction: bool = True,
166
+ firewall_enabled: bool = True,
167
+ loop_detection: bool = True,
168
+ ) -> None:
169
+ """Initialize GovernedAgent.
170
+
171
+ Args:
172
+ upstream: Async callable for upstream calls.
173
+ audit: If True, emit events to the event bus.
174
+ pii_redaction: If True, redact PII bidirectionally.
175
+ firewall_enabled: If True, run injection firewall.
176
+ loop_detection: If True, run loop breaker.
177
+ """
178
+ self._upstream = upstream
179
+ self._audit = audit
180
+ self._pii_redaction = pii_redaction
181
+ self._firewall_enabled = firewall_enabled
182
+ self._loop_detection = loop_detection
183
+ self._firewall: Any = None
184
+ self._loop_breaker: Any = None
185
+ self._pii_redactor: Any = None
186
+
187
+ def _get_firewall(self) -> Any:
188
+ """Return the firewall, creating it lazily."""
189
+ if self._firewall is None:
190
+ self._firewall = _load_firewall()
191
+ return self._firewall
192
+
193
+ def _get_loop_breaker(self) -> Any:
194
+ """Return the loop breaker, creating it lazily."""
195
+ if self._loop_breaker is None:
196
+ self._loop_breaker = _load_loop_breaker()
197
+ return self._loop_breaker
198
+
199
+ def _get_pii_redactor(self) -> Any:
200
+ """Return the PII redactor, creating it lazily."""
201
+ if self._pii_redactor is None:
202
+ self._pii_redactor = _load_pii_redactor()
203
+ return self._pii_redactor
204
+
205
+ async def call(
206
+ self,
207
+ method: str,
208
+ params: dict,
209
+ **kwargs: Any,
210
+ ) -> GovernedMCPResponse:
211
+ """Make a governed upstream call.
212
+
213
+ Runs the governance pipeline on the request, forwards to
214
+ upstream if allowed, then inspects the response bidirectionally.
215
+
216
+ Args:
217
+ method: The RPC method name.
218
+ params: The call parameters.
219
+ **kwargs: Additional arguments (session_id, etc.).
220
+
221
+ Returns:
222
+ GovernedMCPResponse with result, action, and governance info.
223
+ """
224
+ session_id = kwargs.pop("session_id", None) or str(uuid.uuid4())
225
+ start_us = time.time() * 1_000_000
226
+ governance: dict[str, Any] = {}
227
+
228
+ # 1. Emit AGENT_REQUEST event
229
+ if self._audit:
230
+ await bus.emit(
231
+ GovernanceEvent(
232
+ event_type=EventType.AGENT_REQUEST,
233
+ session_id=session_id,
234
+ domain="agent-security",
235
+ metadata={"method": method},
236
+ )
237
+ )
238
+
239
+ text_to_scan = _extract_text(params)
240
+
241
+ # 2. Firewall check
242
+ if self._firewall_enabled and text_to_scan:
243
+ fw_result = self._get_firewall().check(text_to_scan)
244
+ governance["firewall"] = fw_result
245
+ if fw_result.get("is_injection"):
246
+ return await self._blocked_response(
247
+ session_id,
248
+ start_us,
249
+ "BLOCK",
250
+ fw_result.get("risk_level", "HIGH"),
251
+ "firewall",
252
+ governance,
253
+ )
254
+
255
+ # 3. Loop detection
256
+ if self._loop_detection and text_to_scan:
257
+ loop_result = self._get_loop_breaker().check(
258
+ session_id,
259
+ text_to_scan,
260
+ )
261
+ governance["loop_breaker"] = loop_result
262
+ if loop_result.get("is_loop"):
263
+ return await self._blocked_response(
264
+ session_id,
265
+ start_us,
266
+ "CIRCUIT_BREAK",
267
+ "HIGH",
268
+ "loop_breaker",
269
+ governance,
270
+ )
271
+
272
+ # 4. PII redaction on request (inbound)
273
+ redacted_params = params
274
+ if self._pii_redaction:
275
+ redacted_params, pii_count = _redact_dict(
276
+ params,
277
+ self._get_pii_redactor(),
278
+ )
279
+ governance["pii_request"] = {
280
+ "redacted": pii_count > 0,
281
+ "count": pii_count,
282
+ }
283
+
284
+ # 5. Forward to upstream
285
+ upstream_result = await self._upstream(method, redacted_params, **kwargs)
286
+
287
+ # 6. PII redaction on response (outbound)
288
+ redacted_result = upstream_result
289
+ if self._pii_redaction and isinstance(upstream_result, dict):
290
+ redacted_result, pii_count = _redact_dict(
291
+ upstream_result,
292
+ self._get_pii_redactor(),
293
+ )
294
+ governance["pii_response"] = {
295
+ "redacted": pii_count > 0,
296
+ "count": pii_count,
297
+ }
298
+
299
+ latency_us = time.time() * 1_000_000 - start_us
300
+ governance["latency_us"] = latency_us
301
+
302
+ action = "ALLOW"
303
+ pii_req = governance.get("pii_request", {}).get("count", 0)
304
+ pii_resp = governance.get("pii_response", {}).get("count", 0)
305
+ if pii_req > 0 or pii_resp > 0:
306
+ action = "REDACT"
307
+
308
+ # 7. Emit AGENT_RESPONSE event
309
+ if self._audit:
310
+ await bus.emit(
311
+ GovernanceEvent(
312
+ event_type=EventType.AGENT_RESPONSE,
313
+ session_id=session_id,
314
+ domain="agent-security",
315
+ action=action,
316
+ metadata={
317
+ "method": method,
318
+ "latency_us": latency_us,
319
+ },
320
+ )
321
+ )
322
+
323
+ return GovernedMCPResponse(
324
+ result=redacted_result,
325
+ action=action,
326
+ risk_level="LOW",
327
+ governance=governance,
328
+ )
329
+
330
+ async def _blocked_response(
331
+ self,
332
+ session_id: str,
333
+ start_us: float,
334
+ action: str,
335
+ risk_level: Any,
336
+ domain: str,
337
+ governance: dict[str, Any],
338
+ ) -> GovernedMCPResponse:
339
+ """Build a blocked response and emit the event.
340
+
341
+ Args:
342
+ session_id: Session identifier.
343
+ start_us: Start time in microseconds.
344
+ action: The governance action (BLOCK, CIRCUIT_BREAK).
345
+ risk_level: The risk level.
346
+ domain: Which domain blocked the request.
347
+ governance: Governance dict to include.
348
+
349
+ Returns:
350
+ GovernedMCPResponse with the block decision.
351
+ """
352
+ latency_us = time.time() * 1_000_000 - start_us
353
+ governance["latency_us"] = latency_us
354
+
355
+ risk_str = str(risk_level)
356
+ if hasattr(risk_level, "value"):
357
+ risk_str = str(risk_level.value).upper()
358
+ else:
359
+ risk_str = risk_str.upper()
360
+
361
+ if self._audit:
362
+ await bus.emit(
363
+ GovernanceEvent(
364
+ event_type=EventType.AGENT_RESPONSE,
365
+ session_id=session_id,
366
+ domain="agent-security",
367
+ action=action,
368
+ risk_level=risk_str,
369
+ metadata={"domain": domain},
370
+ )
371
+ )
372
+
373
+ return GovernedMCPResponse(
374
+ result=None,
375
+ action=action,
376
+ risk_level=risk_str,
377
+ governance=governance,
378
+ )
379
+
380
+ def call_sync(self, method: str, params: dict, **kwargs: Any) -> GovernedMCPResponse:
381
+ """Synchronous convenience wrapper around call().
382
+
383
+ Args:
384
+ method: The RPC method name.
385
+ params: The call parameters.
386
+ **kwargs: Forwarded to call().
387
+
388
+ Returns:
389
+ GovernedMCPResponse.
390
+ """
391
+ return run_sync(self.call(method, params, **kwargs))