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