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
admina/plugins/base.py ADDED
@@ -0,0 +1,633 @@
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 — 9 plugin abstract base classes.
16
+
17
+ Every extensible capability in Admina is defined as a plugin interface.
18
+ Community developers subclass these ABCs to add support for new models,
19
+ data sources, compliance frameworks, protocols, and more.
20
+
21
+ Install a community plugin::
22
+
23
+ admina plugin install admina-guard-toxicity
24
+
25
+ Or build your own by subclassing any base class below and registering
26
+ it in ``admina.yaml`` under the ``plugins:`` section.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from abc import ABC, abstractmethod
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ if TYPE_CHECKING:
35
+ from fastapi import FastAPI
36
+
37
+ from admina.core.types import GovernanceRequest, GovernanceResponse
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # 1. BaseModelAdapter
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ class BaseModelAdapter(ABC):
46
+ """Interface for LLM / model backends.
47
+
48
+ A model adapter wraps a single inference provider (Ollama, OpenAI,
49
+ Bedrock, vLLM, …) and exposes a uniform ``send()`` method so that
50
+ :class:`GovernedModel` can route prompts through any backend.
51
+
52
+ Default implementations:
53
+ * ``OllamaAdapter`` — local Ollama instance (built-in).
54
+ * ``OpenAIAdapter`` — OpenAI-compatible APIs (built-in).
55
+
56
+ Community plugin example:
57
+ ``admina-adapter-bedrock`` — AWS Bedrock models via boto3.
58
+
59
+ Example usage::
60
+
61
+ class MyAdapter(BaseModelAdapter):
62
+ @property
63
+ def name(self) -> str:
64
+ return "my-backend"
65
+
66
+ def supports_model(self, model_name: str) -> bool:
67
+ return model_name.startswith("my-")
68
+
69
+ async def send(self, prompt, context=None, **kw):
70
+ return {"text": "hello", "metadata": {"tokens": 5}}
71
+ """
72
+
73
+ @abstractmethod
74
+ async def send(
75
+ self,
76
+ prompt: str,
77
+ context: Any = None,
78
+ **kwargs: Any,
79
+ ) -> dict:
80
+ """Send a prompt to the model and return the response.
81
+
82
+ Args:
83
+ prompt: The text prompt to send.
84
+ context: Optional conversation context or system prompt.
85
+ **kwargs: Provider-specific parameters (temperature, etc.).
86
+
87
+ Returns:
88
+ A dict with at least ``{"text": str, "metadata": {"tokens": int,
89
+ "latency_ms": float, ...}}``.
90
+ """
91
+
92
+ @abstractmethod
93
+ def supports_model(self, model_name: str) -> bool:
94
+ """Return ``True`` if this adapter can serve *model_name*.
95
+
96
+ Args:
97
+ model_name: A model identifier, e.g. ``"llama3"`` or ``"gpt-4o"``.
98
+ """
99
+
100
+ @property
101
+ @abstractmethod
102
+ def name(self) -> str:
103
+ """Unique short name for this adapter (e.g. ``"ollama"``)."""
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # 2. BaseDataConnector
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ class BaseDataConnector(ABC):
112
+ """Interface for document / vector-store backends.
113
+
114
+ A data connector knows how to **ingest** documents (chunk, embed,
115
+ store) and **query** them (search, rank, return).
116
+
117
+ Default implementations:
118
+ * ``ChromaDBConnector`` — local ChromaDB (built-in).
119
+ * ``FilesystemConnector`` — plain filesystem (built-in).
120
+
121
+ Community plugin example:
122
+ ``admina-connector-fhir`` — HL7 FHIR clinical data.
123
+
124
+ Example usage::
125
+
126
+ class CsvConnector(BaseDataConnector):
127
+ @property
128
+ def name(self) -> str:
129
+ return "csv"
130
+
131
+ async def ingest(self, source, **kw):
132
+ return {"doc_count": 1, "chunk_count": 42}
133
+
134
+ async def query(self, query, **kw):
135
+ return [{"text": "row1", "metadata": {}, "score": 0.9}]
136
+ """
137
+
138
+ @abstractmethod
139
+ async def ingest(self, source: Any, **kwargs: Any) -> dict:
140
+ """Ingest documents from *source* into the store.
141
+
142
+ Args:
143
+ source: A file path, URL, bytes, or provider-specific locator.
144
+ **kwargs: Provider-specific ingestion options.
145
+
146
+ Returns:
147
+ A dict with at least ``{"doc_count": int, "chunk_count": int}``.
148
+ """
149
+
150
+ @abstractmethod
151
+ async def query(self, query: str, **kwargs: Any) -> list[dict]:
152
+ """Search the store and return ranked results.
153
+
154
+ Args:
155
+ query: The search query string.
156
+ **kwargs: Provider-specific search options (top_k, filters, …).
157
+
158
+ Returns:
159
+ A list of dicts, each with ``{"text": str, "metadata": dict,
160
+ "score": float}``.
161
+ """
162
+
163
+ @property
164
+ @abstractmethod
165
+ def name(self) -> str:
166
+ """Unique short name for this connector (e.g. ``"chromadb"``)."""
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # 3. BaseGovernanceGuard
171
+ # ---------------------------------------------------------------------------
172
+
173
+
174
+ class BaseGovernanceGuard(ABC):
175
+ """Interface for a pluggable governance inspection step in the pipeline.
176
+
177
+ A guard is a single stage that can be added to Admina's governance
178
+ pipeline alongside the built-in steps (loop breaker, firewall, PII).
179
+ Each guard inspects requests and/or responses and returns an action
180
+ decision (ALLOW, BLOCK, REDACT).
181
+
182
+ Guards are auto-discovered from ``plugins/builtin/guards/`` or any
183
+ path listed under ``plugins:`` in ``admina.yaml``. They run in the
184
+ pipeline after the built-in steps.
185
+
186
+ Community plugin examples:
187
+ * ``admina-guard-toxicity`` — ML-based toxic language detection.
188
+ * ``admina-guard-guardrailsai`` — wraps GuardrailsAI validators.
189
+ * ``admina-guard-bias`` — statistical bias detection.
190
+
191
+ Example usage::
192
+
193
+ class ToxicityGuard(BaseGovernanceGuard):
194
+ name = "toxicity"
195
+
196
+ async def inspect_request(self, request):
197
+ # analyse request content ...
198
+ return {"action": "ALLOW", "risk_level": "LOW", "details": ""}
199
+
200
+ async def inspect_response(self, response):
201
+ return {"action": "ALLOW", "risk_level": "LOW", "details": ""}
202
+ """
203
+
204
+ @abstractmethod
205
+ async def inspect_request(self, request: dict) -> dict:
206
+ """Inspect an inbound request before it reaches the model.
207
+
208
+ Args:
209
+ request: Governance request payload dict. At minimum contains
210
+ ``"content"`` (the raw text) and ``"params"`` (tool args).
211
+
212
+ Returns:
213
+ A dict with ``{"action": "ALLOW"|"BLOCK"|"REDACT",
214
+ "risk_level": str, "details": str}``.
215
+ """
216
+
217
+ @abstractmethod
218
+ async def inspect_response(self, response: dict) -> dict:
219
+ """Inspect the model's response before it reaches the caller.
220
+
221
+ Args:
222
+ response: Governance response payload dict.
223
+
224
+ Returns:
225
+ Same structure as :meth:`inspect_request`.
226
+ """
227
+
228
+ @property
229
+ @abstractmethod
230
+ def name(self) -> str:
231
+ """Unique short name for this guard (e.g. ``"toxicity"``)."""
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # 4. BaseComplianceTemplate
236
+ # ---------------------------------------------------------------------------
237
+
238
+
239
+ class BaseComplianceTemplate(ABC):
240
+ """Interface for regulatory compliance frameworks.
241
+
242
+ A compliance template defines a set of requirements (articles,
243
+ controls, checks) and can evaluate the current governance state
244
+ against them to produce a gap analysis.
245
+
246
+ Default implementations:
247
+ * ``EUAIActTemplate`` — EU AI Act Art. 6-15 (built-in).
248
+ * ``GDPRTemplate`` — GDPR basics (built-in).
249
+
250
+ Community plugin example:
251
+ ``admina-compliance-hipaa`` — US HIPAA compliance checks.
252
+
253
+ Example usage::
254
+
255
+ class SOC2Template(BaseComplianceTemplate):
256
+ @property
257
+ def framework_name(self) -> str:
258
+ return "SOC2"
259
+
260
+ def get_requirements(self):
261
+ return [{"id": "cc1", "title": "Control environment",
262
+ "checks": []}]
263
+
264
+ def evaluate(self, governance_state):
265
+ return {"score": 0.75, "gaps": ["cc1"], "covered": []}
266
+ """
267
+
268
+ @abstractmethod
269
+ def get_requirements(self) -> list[dict]:
270
+ """Return the list of requirements for this framework.
271
+
272
+ Returns:
273
+ A list of dicts, each with ``{"id": str, "title": str,
274
+ "checks": list[callable]}``.
275
+ """
276
+
277
+ @abstractmethod
278
+ def evaluate(self, governance_state: dict) -> dict:
279
+ """Evaluate current governance state against the requirements.
280
+
281
+ Args:
282
+ governance_state: A dict describing the system's governance
283
+ posture (active domains, configuration, recent events).
284
+
285
+ Returns:
286
+ A dict with ``{"score": float, "gaps": list, "covered": list}``.
287
+ """
288
+
289
+ @property
290
+ @abstractmethod
291
+ def framework_name(self) -> str:
292
+ """The compliance framework name (e.g. ``"EU AI Act"``)."""
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # 5. BaseTransportAdapter
297
+ # ---------------------------------------------------------------------------
298
+
299
+
300
+ class BaseTransportAdapter(ABC):
301
+ """Interface for wire-protocol adapters.
302
+
303
+ A transport adapter converts between a protocol-specific wire format
304
+ (MCP JSON-RPC, A2A, REST, AG-UI, …) and the protocol-agnostic
305
+ :class:`GovernanceRequest` / :class:`GovernanceResponse` dataclasses.
306
+
307
+ This is the key to Admina's protocol independence: the governance
308
+ engine never sees wire format, only ``GovernanceRequest`` objects.
309
+
310
+ Default implementations:
311
+ * ``MCPTransportAdapter`` — JSON-RPC 2.0 (built-in).
312
+ * ``HTTPRESTTransportAdapter`` — plain REST (built-in).
313
+
314
+ Community plugin examples:
315
+ * ``admina-transport-a2a`` — Google A2A protocol.
316
+ * ``admina-transport-ag-ui`` — AG-UI streaming protocol.
317
+
318
+ Example usage::
319
+
320
+ class GRPCTransport(BaseTransportAdapter):
321
+ @property
322
+ def protocol_name(self) -> str:
323
+ return "grpc"
324
+
325
+ async def parse_request(self, raw_request):
326
+ ... # deserialize protobuf → GovernanceRequest
327
+
328
+ async def format_response(self, gov_response, original):
329
+ ... # GovernanceResponse → protobuf
330
+
331
+ def register_routes(self, app):
332
+ ... # mount gRPC service
333
+ """
334
+
335
+ @abstractmethod
336
+ async def parse_request(self, raw_request: Any) -> GovernanceRequest:
337
+ """Normalize a protocol-specific request into a GovernanceRequest.
338
+
339
+ Args:
340
+ raw_request: The raw incoming request (e.g. Starlette Request,
341
+ bytes, dict).
342
+
343
+ Returns:
344
+ A :class:`GovernanceRequest` instance.
345
+ """
346
+
347
+ @abstractmethod
348
+ async def format_response(
349
+ self,
350
+ gov_response: GovernanceResponse,
351
+ original: Any,
352
+ ) -> Any:
353
+ """Convert a GovernanceResponse back to protocol-specific format.
354
+
355
+ Args:
356
+ gov_response: The governance engine's decision.
357
+ original: The original raw request, for correlation.
358
+
359
+ Returns:
360
+ A protocol-specific response object.
361
+ """
362
+
363
+ @abstractmethod
364
+ def register_routes(self, app: FastAPI) -> None:
365
+ """Register protocol-specific HTTP routes on the FastAPI app.
366
+
367
+ Args:
368
+ app: The FastAPI application instance.
369
+
370
+ Example:
371
+ ``app.add_api_route("/mcp", self.handle, methods=["POST"])``
372
+ """
373
+
374
+ @property
375
+ @abstractmethod
376
+ def protocol_name(self) -> str:
377
+ """Protocol identifier (e.g. ``"mcp"``, ``"a2a"``, ``"rest"``)."""
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # 6. BaseForensicStore
382
+ # ---------------------------------------------------------------------------
383
+
384
+
385
+ class BaseForensicStore(ABC):
386
+ """Interface for forensic audit-trail storage backends.
387
+
388
+ A forensic store persists governance records into an append-only
389
+ log. The SHA-256 hash-chain logic lives in the core engine — the
390
+ store only handles I/O (write, read-back, verify).
391
+
392
+ Default implementations:
393
+ * ``MinIOForensicStore`` — S3-compatible object storage (built-in).
394
+ * ``FilesystemForensicStore`` — local JSON files (built-in).
395
+
396
+ Community plugin example:
397
+ ``admina-forensic-azure-blob`` — Azure Blob Storage backend.
398
+
399
+ Example usage::
400
+
401
+ class SQLiteForensicStore(BaseForensicStore):
402
+ @property
403
+ def store_name(self) -> str:
404
+ return "sqlite"
405
+
406
+ async def append(self, record):
407
+ ... # INSERT INTO forensic_log
408
+ return "row-id-123"
409
+
410
+ async def verify_chain(self, last_n=0):
411
+ return {"valid": True, "records": 100, "last_hash": "ab12..."}
412
+ """
413
+
414
+ @abstractmethod
415
+ async def append(self, record: dict) -> str:
416
+ """Write a governance record to the store.
417
+
418
+ The hash-chain computation is done by the core engine before
419
+ calling this method; the store simply persists the record.
420
+
421
+ Args:
422
+ record: The governance record dict (includes hash fields).
423
+
424
+ Returns:
425
+ A storage-specific identifier for the written record.
426
+ """
427
+
428
+ @abstractmethod
429
+ async def verify_chain(self, last_n: int = 0) -> dict:
430
+ """Verify the integrity of the hash chain.
431
+
432
+ Args:
433
+ last_n: If > 0, verify only the last *n* records.
434
+ If 0, verify the entire chain.
435
+
436
+ Returns:
437
+ A dict with ``{"valid": bool, "records": int,
438
+ "last_hash": str}``.
439
+ """
440
+
441
+ @property
442
+ @abstractmethod
443
+ def store_name(self) -> str:
444
+ """Unique short name for this store (e.g. ``"minio"``)."""
445
+
446
+
447
+ # ---------------------------------------------------------------------------
448
+ # 7. BaseAuthProvider
449
+ # ---------------------------------------------------------------------------
450
+
451
+
452
+ class BaseAuthProvider(ABC):
453
+ """Interface for authentication and authorization providers.
454
+
455
+ An auth provider handles identity verification (authenticate) and
456
+ permission checks (authorize) for incoming requests.
457
+
458
+ Default implementations:
459
+ * ``APIKeyAuthProvider`` — simple API-key auth (built-in).
460
+
461
+ Community plugin example:
462
+ ``admina-auth-ldap`` — LDAP / Active Directory authentication.
463
+
464
+ Example usage::
465
+
466
+ class JWTAuthProvider(BaseAuthProvider):
467
+ @property
468
+ def provider_name(self) -> str:
469
+ return "jwt"
470
+
471
+ async def authenticate(self, request):
472
+ ... # decode JWT, verify signature
473
+ return {"user_id": "u1", "roles": ["admin"], "metadata": {}}
474
+
475
+ async def authorize(self, user, action, resource=""):
476
+ return "admin" in user["roles"]
477
+ """
478
+
479
+ @abstractmethod
480
+ async def authenticate(self, request: Any) -> dict:
481
+ """Authenticate an incoming request.
482
+
483
+ Args:
484
+ request: The raw request object (e.g. Starlette Request).
485
+
486
+ Returns:
487
+ A dict with ``{"user_id": str, "roles": list, "metadata": dict}``.
488
+
489
+ Raises:
490
+ Exception: If authentication fails.
491
+ """
492
+
493
+ @abstractmethod
494
+ async def authorize(
495
+ self,
496
+ user: dict,
497
+ action: str,
498
+ resource: str = "",
499
+ ) -> bool:
500
+ """Check whether *user* is allowed to perform *action*.
501
+
502
+ Args:
503
+ user: The dict returned by :meth:`authenticate`.
504
+ action: The action being attempted (e.g. ``"model.call"``).
505
+ resource: Optional resource identifier.
506
+
507
+ Returns:
508
+ ``True`` if authorized, ``False`` otherwise.
509
+ """
510
+
511
+ @property
512
+ @abstractmethod
513
+ def provider_name(self) -> str:
514
+ """Unique short name for this provider (e.g. ``"apikey"``)."""
515
+
516
+
517
+ # ---------------------------------------------------------------------------
518
+ # 8. BasePIIEngine
519
+ # ---------------------------------------------------------------------------
520
+
521
+
522
+ class BasePIIEngine(ABC):
523
+ """Interface for PII detection and redaction engines.
524
+
525
+ A PII engine detects personally identifiable information in text
526
+ and redacts it with placeholders like ``[EMAIL]``, ``[PERSON]``.
527
+
528
+ Default implementations:
529
+ * ``SpaCyRegexPIIEngine`` — spaCy NER + regex rules (built-in).
530
+
531
+ Community plugin example:
532
+ ``admina-pii-presidio`` — Microsoft Presidio with 10+ language
533
+ support and ML-based entity recognition.
534
+
535
+ Example usage::
536
+
537
+ class SimplePII(BasePIIEngine):
538
+ @property
539
+ def supported_languages(self) -> list[str]:
540
+ return ["en"]
541
+
542
+ async def detect(self, text, categories=None):
543
+ return [{"type": "EMAIL", "start": 10, "end": 25,
544
+ "text": "foo@bar.com", "confidence": 0.99}]
545
+
546
+ async def redact(self, text, matches):
547
+ return text[:10] + "[EMAIL]" + text[25:]
548
+ """
549
+
550
+ @abstractmethod
551
+ async def detect(
552
+ self,
553
+ text: str,
554
+ categories: list[str] | None = None,
555
+ ) -> list[dict]:
556
+ """Detect PII entities in *text*.
557
+
558
+ Args:
559
+ text: The input text to scan.
560
+ categories: Optional list of PII types to detect (e.g.
561
+ ``["EMAIL", "PERSON"]``). ``None`` means detect all.
562
+
563
+ Returns:
564
+ A list of dicts, each with ``{"type": str, "start": int,
565
+ "end": int, "text": str, "confidence": float}``.
566
+ """
567
+
568
+ @abstractmethod
569
+ async def redact(self, text: str, matches: list[dict]) -> str:
570
+ """Replace detected PII spans with type-based placeholders.
571
+
572
+ Args:
573
+ text: The original text.
574
+ matches: The list returned by :meth:`detect`.
575
+
576
+ Returns:
577
+ The redacted text with placeholders like ``[EMAIL]``, ``[PERSON]``.
578
+ """
579
+
580
+ @property
581
+ @abstractmethod
582
+ def supported_languages(self) -> list[str]:
583
+ """ISO 639-1 codes of languages this engine supports."""
584
+
585
+
586
+ # ---------------------------------------------------------------------------
587
+ # 9. BaseAlertChannel
588
+ # ---------------------------------------------------------------------------
589
+
590
+
591
+ class BaseAlertChannel(ABC):
592
+ """Interface for governance alert delivery channels.
593
+
594
+ An alert channel delivers governance notifications (blocked requests,
595
+ compliance gaps, chain verification failures, …) to operators via
596
+ their preferred medium.
597
+
598
+ Default implementations:
599
+ * ``LogAlertChannel`` — Python logger (built-in).
600
+ * ``WebhookAlertChannel`` — HTTP POST to a URL (built-in).
601
+
602
+ Community plugin example:
603
+ ``admina-alert-slack`` — Slack webhook with rich formatting.
604
+
605
+ Example usage::
606
+
607
+ class EmailAlert(BaseAlertChannel):
608
+ @property
609
+ def channel_name(self) -> str:
610
+ return "email"
611
+
612
+ async def send_alert(self, alert):
613
+ ... # send via SMTP
614
+ return True
615
+ """
616
+
617
+ @abstractmethod
618
+ async def send_alert(self, alert: dict) -> bool:
619
+ """Send a governance alert.
620
+
621
+ Args:
622
+ alert: A dict with ``{"level": str, "domain": str,
623
+ "summary": str, "details": dict,
624
+ "timestamp": datetime}``.
625
+
626
+ Returns:
627
+ ``True`` if the alert was delivered successfully.
628
+ """
629
+
630
+ @property
631
+ @abstractmethod
632
+ def channel_name(self) -> str:
633
+ """Unique short name for this channel (e.g. ``"slack"``)."""
@@ -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
+