admina-framework 0.9.5__tar.gz → 0.10.1__tar.gz
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_framework-0.9.5 → admina_framework-0.10.1}/NOTICE +1 -1
- {admina_framework-0.9.5/admina_framework.egg-info → admina_framework-0.10.1}/PKG-INFO +43 -6
- {admina_framework-0.9.5 → admina_framework-0.10.1}/README.md +22 -1
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/__init__.py +1 -1
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/main.py +67 -2
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/docker-compose.yml.j2 +3 -3
- admina_framework-0.10.1/admina/cli/templates/plugin.py.j2 +117 -0
- admina_framework-0.10.1/admina/cli/templates/plugin_pyproject.toml.j2 +21 -0
- admina_framework-0.10.1/admina/cli/templates/plugin_test.py.j2 +27 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/core/config.py +17 -2
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/ai_infra/rag.py +49 -6
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/eu_ai_act.py +26 -2
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/forensic.py +77 -28
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/oisg.py +17 -5
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/data_sovereignty/pii.py +18 -0
- {admina_framework-0.9.5/admina/proxy → admina_framework-0.10.1/admina/domains}/governance.py +81 -11
- admina_framework-0.10.1/admina/engines/__init__.py +431 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/_engines.py +12 -9
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/base.py +16 -4
- admina_framework-0.10.1/admina/plugins/builtin/adapters/anthropic.py +142 -0
- admina_framework-0.10.1/admina/plugins/builtin/adapters/bedrock.py +157 -0
- admina_framework-0.10.1/admina/plugins/builtin/adapters/gemini.py +137 -0
- admina_framework-0.10.1/admina/plugins/builtin/adapters/mistral.py +143 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/adapters/ollama.py +6 -6
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/adapters/openai.py +6 -6
- admina_framework-0.10.1/admina/plugins/builtin/adapters/vllm.py +85 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/auth/apikey.py +59 -17
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/compliance/eu_ai_act.py +2 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/connectors/chromadb.py +2 -5
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/connectors/filesystem.py +2 -5
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/forensic/filesystem.py +94 -26
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/pii/spacy_regex.py +30 -3
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/registry.py +19 -16
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/api/dashboard.py +28 -13
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/api/integration.py +89 -39
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/config.py +4 -0
- admina_framework-0.10.1/admina/proxy/engine_bridge.py +44 -0
- admina_framework-0.10.1/admina/proxy/governance.py +37 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/main.py +190 -65
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/sdk/__init__.py +6 -0
- admina_framework-0.10.1/admina/sdk/errors.py +27 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/sdk/governed_agent.py +47 -103
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/sdk/governed_data.py +38 -40
- admina_framework-0.10.1/admina/sdk/governed_model.py +325 -0
- admina_framework-0.10.1/admina/sdk/retry.py +127 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1/admina_framework.egg-info}/PKG-INFO +43 -6
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina_framework.egg-info/SOURCES.txt +13 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina_framework.egg-info/requires.txt +28 -4
- {admina_framework-0.9.5 → admina_framework-0.10.1}/pyproject.toml +17 -6
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_core_config.py +56 -9
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_domains.py +17 -2
- admina_framework-0.10.1/tests/test_domains_governance.py +317 -0
- admina_framework-0.10.1/tests/test_engines.py +309 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_forensic_blackbox.py +161 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_governance_pipeline.py +9 -3
- admina_framework-0.10.1/tests/test_proxy_mcp_response_pii.py +149 -0
- admina_framework-0.10.1/tests/test_proxy_plugins.py +93 -0
- admina_framework-0.10.1/tests/test_proxy_security.py +462 -0
- admina_framework-0.9.5/admina/cli/templates/plugin.py.j2 +0 -145
- admina_framework-0.9.5/admina/cli/templates/plugin_pyproject.toml.j2 +0 -15
- admina_framework-0.9.5/admina/cli/templates/plugin_test.py.j2 +0 -48
- admina_framework-0.9.5/admina/proxy/engine_bridge.py +0 -251
- admina_framework-0.9.5/admina/sdk/governed_model.py +0 -243
- admina_framework-0.9.5/tests/test_proxy_security.py +0 -210
- {admina_framework-0.9.5 → admina_framework-0.10.1}/LICENSE +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/commands/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/admina.yaml.j2 +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/env.j2 +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/main.py.j2 +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/plugin_readme.md.j2 +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/core/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/core/event_bus.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/core/secrets.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/core/types.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/dashboard/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/dashboard/static/heimdall.png +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/dashboard/static/index.html +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/agent_security/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/agent_security/firewall.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/agent_security/loop_breaker.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/ai_infra/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/ai_infra/llm_engine.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/ai_infra/webui.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/cross_regulation.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/gdpr.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/nis2.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/compliance/otel.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/data_sovereignty/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/data_sovereignty/classification.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/domains/data_sovereignty/residency.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/cheshirecat/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/crewai/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/crewai/callbacks.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/langchain/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/langchain/callbacks.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/n8n/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/integrations/openclaw/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/adapters/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/alerts/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/alerts/log.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/alerts/webhook.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/auth/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/compliance/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/connectors/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/forensic/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/guards/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/guards/guardrailsai_guard.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/pii/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/transports/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/transports/http_rest.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/plugins/builtin/transports/mcp.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/api/__init__.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/multi_upstream.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/proxy/state.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/py.typed +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/sdk/_compat.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina/sdk/compliance_kit.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina_framework.egg-info/dependency_links.txt +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina_framework.egg-info/entry_points.txt +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/admina_framework.egg-info/top_level.txt +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/setup.cfg +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_benchmark_14us.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_firewall_parity.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_mcp_transport.py +0 -0
- {admina_framework-0.9.5 → admina_framework-0.10.1}/tests/test_version.py +0 -0
|
@@ -9,7 +9,7 @@ Source: https://github.com/admina-org/admina
|
|
|
9
9
|
|
|
10
10
|
─── Bundled assets ─────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
* dashboard/heimdall.png
|
|
12
|
+
* admina/dashboard/static/heimdall.png
|
|
13
13
|
"Heimdall — the Governance Owl"
|
|
14
14
|
Copyright © 2025–2026 Stefano Noferi & Admina contributors
|
|
15
15
|
Licensed under the Apache License, Version 2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: admina-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Admina — governed AI development framework
|
|
5
5
|
Author-email: Stefano Noferi <info@admina.org>
|
|
6
6
|
Maintainer-email: Stefano Noferi <info@admina.org>
|
|
@@ -30,7 +30,7 @@ License-File: NOTICE
|
|
|
30
30
|
Requires-Dist: pyyaml<7,>=6.0
|
|
31
31
|
Requires-Dist: click<9,>=8.1
|
|
32
32
|
Requires-Dist: jinja2<4,>=3.1
|
|
33
|
-
Requires-Dist: cryptography<
|
|
33
|
+
Requires-Dist: cryptography<50,>=41.0
|
|
34
34
|
Provides-Extra: proxy
|
|
35
35
|
Requires-Dist: fastapi<1,>=0.104; extra == "proxy"
|
|
36
36
|
Requires-Dist: uvicorn[standard]<1,>=0.24; extra == "proxy"
|
|
@@ -38,9 +38,9 @@ Requires-Dist: httpx<1,>=0.25; extra == "proxy"
|
|
|
38
38
|
Requires-Dist: pydantic<3,>=2.5; extra == "proxy"
|
|
39
39
|
Requires-Dist: pydantic-settings<3,>=2.3; extra == "proxy"
|
|
40
40
|
Requires-Dist: python-dotenv<2,>=1.0; extra == "proxy"
|
|
41
|
-
Requires-Dist: redis<
|
|
41
|
+
Requires-Dist: redis<9,>=5.0; extra == "proxy"
|
|
42
42
|
Requires-Dist: boto3<2,>=1.34; extra == "proxy"
|
|
43
|
-
Requires-Dist: clickhouse-connect<
|
|
43
|
+
Requires-Dist: clickhouse-connect<2,>=0.7; extra == "proxy"
|
|
44
44
|
Requires-Dist: typer<1,>=0.9; extra == "proxy"
|
|
45
45
|
Requires-Dist: numpy<2,>=1.24; extra == "proxy"
|
|
46
46
|
Requires-Dist: scikit-learn<2,>=1.3; extra == "proxy"
|
|
@@ -52,9 +52,25 @@ Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "telemetry"
|
|
|
52
52
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2,>=1.20; extra == "telemetry"
|
|
53
53
|
Requires-Dist: opentelemetry-instrumentation-fastapi<1,>=0.40b0; extra == "telemetry"
|
|
54
54
|
Provides-Extra: rust
|
|
55
|
-
Requires-Dist: admina-core<0.
|
|
55
|
+
Requires-Dist: admina-core<0.11,>=0.9.3; extra == "rust"
|
|
56
56
|
Provides-Extra: full
|
|
57
57
|
Requires-Dist: admina-framework[nlp,proxy,telemetry]; extra == "full"
|
|
58
|
+
Provides-Extra: openai
|
|
59
|
+
Requires-Dist: openai<3,>=1.0; extra == "openai"
|
|
60
|
+
Provides-Extra: ollama
|
|
61
|
+
Requires-Dist: ollama<1,>=0.3; extra == "ollama"
|
|
62
|
+
Provides-Extra: anthropic
|
|
63
|
+
Requires-Dist: anthropic<1,>=0.39; extra == "anthropic"
|
|
64
|
+
Provides-Extra: mistral
|
|
65
|
+
Requires-Dist: mistralai<3,>=1.0; extra == "mistral"
|
|
66
|
+
Provides-Extra: gemini
|
|
67
|
+
Requires-Dist: google-genai<3,>=1.0; extra == "gemini"
|
|
68
|
+
Provides-Extra: bedrock
|
|
69
|
+
Requires-Dist: boto3<2,>=1.34; extra == "bedrock"
|
|
70
|
+
Provides-Extra: adapters
|
|
71
|
+
Requires-Dist: admina-framework[anthropic,bedrock,gemini,mistral,ollama,openai]; extra == "adapters"
|
|
72
|
+
Provides-Extra: all
|
|
73
|
+
Requires-Dist: admina-framework[adapters,nlp,proxy,telemetry]; extra == "all"
|
|
58
74
|
Dynamic: license-file
|
|
59
75
|
|
|
60
76
|
<!--
|
|
@@ -186,6 +202,21 @@ pip install "admina-framework[proxy]"
|
|
|
186
202
|
pip install "admina-framework[full]"
|
|
187
203
|
python -m spacy download en_core_web_sm # for [full] only
|
|
188
204
|
|
|
205
|
+
# Model-adapter provider SDKs (per-provider extras):
|
|
206
|
+
pip install "admina-framework[openai]" # openai>=1.0
|
|
207
|
+
pip install "admina-framework[ollama]" # ollama>=0.3
|
|
208
|
+
pip install "admina-framework[anthropic]" # anthropic>=0.39
|
|
209
|
+
pip install "admina-framework[mistral]" # mistralai>=1.0
|
|
210
|
+
pip install "admina-framework[gemini]" # google-genai>=1.0
|
|
211
|
+
pip install "admina-framework[bedrock]" # boto3>=1.34 (AWS Bedrock)
|
|
212
|
+
|
|
213
|
+
# All provider SDKs at once:
|
|
214
|
+
pip install "admina-framework[adapters]"
|
|
215
|
+
|
|
216
|
+
# Everything (proxy + NLP + telemetry + all adapters):
|
|
217
|
+
pip install "admina-framework[all]"
|
|
218
|
+
python -m spacy download en_core_web_sm # for [all] only
|
|
219
|
+
|
|
189
220
|
# Optional: Rust-accelerated engine (auto-detected at runtime).
|
|
190
221
|
# Opt-in extra — pulls in the admina-core wheel from PyPI.
|
|
191
222
|
pip install "admina-framework[rust]"
|
|
@@ -224,6 +255,12 @@ pip install -e ".[proxy]"
|
|
|
224
255
|
# Everything (proxy + NLP + telemetry)
|
|
225
256
|
pip install -e ".[full]"
|
|
226
257
|
|
|
258
|
+
# All model-adapter SDKs
|
|
259
|
+
pip install -e ".[adapters]"
|
|
260
|
+
|
|
261
|
+
# Everything (proxy + NLP + telemetry + all adapters)
|
|
262
|
+
pip install -e ".[all]"
|
|
263
|
+
|
|
227
264
|
# CLI workflow
|
|
228
265
|
admina init my-project # Scaffold a governed AI project
|
|
229
266
|
cd my-project # admina dev runs from the project directory
|
|
@@ -258,7 +295,7 @@ flowchart LR
|
|
|
258
295
|
class P pipe;
|
|
259
296
|
```
|
|
260
297
|
|
|
261
|
-
Pipeline (identical in both modes): `
|
|
298
|
+
Pipeline (identical in both modes): `loop-breaker → firewall → PII redaction → guards → audit → forensic chain (SHA-256) → OTEL`
|
|
262
299
|
|
|
263
300
|
## The 4 Governance Domains
|
|
264
301
|
|
|
@@ -127,6 +127,21 @@ pip install "admina-framework[proxy]"
|
|
|
127
127
|
pip install "admina-framework[full]"
|
|
128
128
|
python -m spacy download en_core_web_sm # for [full] only
|
|
129
129
|
|
|
130
|
+
# Model-adapter provider SDKs (per-provider extras):
|
|
131
|
+
pip install "admina-framework[openai]" # openai>=1.0
|
|
132
|
+
pip install "admina-framework[ollama]" # ollama>=0.3
|
|
133
|
+
pip install "admina-framework[anthropic]" # anthropic>=0.39
|
|
134
|
+
pip install "admina-framework[mistral]" # mistralai>=1.0
|
|
135
|
+
pip install "admina-framework[gemini]" # google-genai>=1.0
|
|
136
|
+
pip install "admina-framework[bedrock]" # boto3>=1.34 (AWS Bedrock)
|
|
137
|
+
|
|
138
|
+
# All provider SDKs at once:
|
|
139
|
+
pip install "admina-framework[adapters]"
|
|
140
|
+
|
|
141
|
+
# Everything (proxy + NLP + telemetry + all adapters):
|
|
142
|
+
pip install "admina-framework[all]"
|
|
143
|
+
python -m spacy download en_core_web_sm # for [all] only
|
|
144
|
+
|
|
130
145
|
# Optional: Rust-accelerated engine (auto-detected at runtime).
|
|
131
146
|
# Opt-in extra — pulls in the admina-core wheel from PyPI.
|
|
132
147
|
pip install "admina-framework[rust]"
|
|
@@ -165,6 +180,12 @@ pip install -e ".[proxy]"
|
|
|
165
180
|
# Everything (proxy + NLP + telemetry)
|
|
166
181
|
pip install -e ".[full]"
|
|
167
182
|
|
|
183
|
+
# All model-adapter SDKs
|
|
184
|
+
pip install -e ".[adapters]"
|
|
185
|
+
|
|
186
|
+
# Everything (proxy + NLP + telemetry + all adapters)
|
|
187
|
+
pip install -e ".[all]"
|
|
188
|
+
|
|
168
189
|
# CLI workflow
|
|
169
190
|
admina init my-project # Scaffold a governed AI project
|
|
170
191
|
cd my-project # admina dev runs from the project directory
|
|
@@ -199,7 +220,7 @@ flowchart LR
|
|
|
199
220
|
class P pipe;
|
|
200
221
|
```
|
|
201
222
|
|
|
202
|
-
Pipeline (identical in both modes): `
|
|
223
|
+
Pipeline (identical in both modes): `loop-breaker → firewall → PII redaction → guards → audit → forensic chain (SHA-256) → OTEL`
|
|
203
224
|
|
|
204
225
|
## The 4 Governance Domains
|
|
205
226
|
|
|
@@ -38,6 +38,8 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
38
38
|
from admina import __version__
|
|
39
39
|
from admina.core.secrets import SecretVault, validate_password
|
|
40
40
|
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
41
43
|
_TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
42
44
|
|
|
43
45
|
# Domains the user can toggle on/off during ``admina init``.
|
|
@@ -166,6 +168,7 @@ def _scaffold_project(
|
|
|
166
168
|
context: dict[str, object] = {
|
|
167
169
|
"project_name": project_name,
|
|
168
170
|
"domains": {d: d in domains for d in AVAILABLE_DOMAINS},
|
|
171
|
+
"admina_version": __version__,
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
# admina.yaml
|
|
@@ -398,6 +401,7 @@ def _maybe_regenerate_compose(project_dir: Path, data: dict[str, object]) -> boo
|
|
|
398
401
|
"project_name": project_name,
|
|
399
402
|
"domains": {d: d in domains for d in AVAILABLE_DOMAINS},
|
|
400
403
|
"with_llm": True, # init scaffolds the full template; admina dev gates at runtime
|
|
404
|
+
"admina_version": __version__,
|
|
401
405
|
}
|
|
402
406
|
_generate_file(env, "docker-compose.yml.j2", project_dir / "docker-compose.yml", context)
|
|
403
407
|
hash_file.write_text(current_hash)
|
|
@@ -686,6 +690,7 @@ def _run_compose(
|
|
|
686
690
|
"project_name": project_name,
|
|
687
691
|
"domains": {d: d in domains for d in AVAILABLE_DOMAINS},
|
|
688
692
|
"with_llm": with_llm,
|
|
693
|
+
"admina_version": __version__,
|
|
689
694
|
}
|
|
690
695
|
_generate_file(env_j, "docker-compose.yml.j2", project_dir / "docker-compose.yml", context)
|
|
691
696
|
hash_file.write_text(current_hash)
|
|
@@ -885,12 +890,23 @@ def _pip_install(package: str) -> subprocess.CompletedProcess[str]:
|
|
|
885
890
|
)
|
|
886
891
|
|
|
887
892
|
|
|
893
|
+
def _config_extra_modules() -> list[str]:
|
|
894
|
+
"""Module paths from admina.yaml ``plugins:`` — empty if no config."""
|
|
895
|
+
try:
|
|
896
|
+
from admina.core.config import load_config
|
|
897
|
+
|
|
898
|
+
return list(load_config().plugins)
|
|
899
|
+
except (ImportError, ValueError, OSError) as exc:
|
|
900
|
+
logger.debug("Could not read plugins from admina.yaml: %s", exc)
|
|
901
|
+
return []
|
|
902
|
+
|
|
903
|
+
|
|
888
904
|
def _discover_and_list_plugins() -> dict[str, dict[str, type]]:
|
|
889
905
|
"""Run plugin discovery and return all registered plugins by type."""
|
|
890
906
|
from admina.plugins.registry import PluginRegistry
|
|
891
907
|
|
|
892
908
|
registry = PluginRegistry()
|
|
893
|
-
registry.discover()
|
|
909
|
+
registry.discover(extra_modules=_config_extra_modules() or None)
|
|
894
910
|
return registry.list_all()
|
|
895
911
|
|
|
896
912
|
|
|
@@ -915,6 +931,9 @@ def _scaffold_plugin(
|
|
|
915
931
|
"base_class": base_class,
|
|
916
932
|
"class_name": class_name,
|
|
917
933
|
"name_property": name_prop,
|
|
934
|
+
"needs_any": plugin_type
|
|
935
|
+
in {"model_adapter", "data_connector", "transport_adapter", "auth_provider"},
|
|
936
|
+
"admina_version": __version__,
|
|
918
937
|
}
|
|
919
938
|
|
|
920
939
|
created: list[str] = []
|
|
@@ -1215,7 +1234,7 @@ def doctor() -> None:
|
|
|
1215
1234
|
from admina.plugins.registry import PluginRegistry
|
|
1216
1235
|
|
|
1217
1236
|
reg = PluginRegistry()
|
|
1218
|
-
count = reg.discover()
|
|
1237
|
+
count = reg.discover(extra_modules=_config_extra_modules() or None)
|
|
1219
1238
|
click.echo(f" Discovered {ok_mark} {count} plugins")
|
|
1220
1239
|
except Exception as exc:
|
|
1221
1240
|
import traceback
|
|
@@ -1327,6 +1346,52 @@ def doctor() -> None:
|
|
|
1327
1346
|
" proxy /health -- not reachable (stack down or different port)"
|
|
1328
1347
|
)
|
|
1329
1348
|
|
|
1349
|
+
# ── Forensic hash-chain integrity ───────────────────────
|
|
1350
|
+
click.echo("\n Forensic chain:")
|
|
1351
|
+
forensic_backend = env.get("FORENSIC_BACKEND", os.environ.get("FORENSIC_BACKEND", "memory"))
|
|
1352
|
+
if forensic_backend in ("filesystem", "s3"):
|
|
1353
|
+
try:
|
|
1354
|
+
import asyncio as _asyncio
|
|
1355
|
+
|
|
1356
|
+
from admina.domains.compliance.forensic import ForensicBlackBox
|
|
1357
|
+
|
|
1358
|
+
if forensic_backend == "filesystem":
|
|
1359
|
+
base_dir = env.get("FORENSIC_BASE_DIR", os.environ.get("FORENSIC_BASE_DIR", ""))
|
|
1360
|
+
fbox = ForensicBlackBox(filesystem_dir=base_dir if base_dir else None)
|
|
1361
|
+
else:
|
|
1362
|
+
# s3 — needs boto3; construct without credentials (probe only)
|
|
1363
|
+
try:
|
|
1364
|
+
import boto3 as _boto3
|
|
1365
|
+
except ImportError:
|
|
1366
|
+
raise ImportError("boto3 not installed")
|
|
1367
|
+
s3_endpoint = env.get(
|
|
1368
|
+
"FORENSIC_S3_ENDPOINT", os.environ.get("FORENSIC_S3_ENDPOINT", "")
|
|
1369
|
+
)
|
|
1370
|
+
s3_bucket = env.get(
|
|
1371
|
+
"FORENSIC_S3_BUCKET", os.environ.get("FORENSIC_S3_BUCKET", "admina-forensic")
|
|
1372
|
+
)
|
|
1373
|
+
s3_kwargs: dict = {}
|
|
1374
|
+
if s3_endpoint:
|
|
1375
|
+
s3_kwargs["endpoint_url"] = s3_endpoint
|
|
1376
|
+
client = _boto3.client("s3", **s3_kwargs)
|
|
1377
|
+
fbox = ForensicBlackBox(boto3_client=client, bucket=s3_bucket)
|
|
1378
|
+
|
|
1379
|
+
chain = _asyncio.run(fbox.verify_chain())
|
|
1380
|
+
n = chain.get("records", 0)
|
|
1381
|
+
if chain.get("valid"):
|
|
1382
|
+
click.echo(f" {forensic_backend:20s} {ok_mark} {n} records, valid")
|
|
1383
|
+
else:
|
|
1384
|
+
click.echo(
|
|
1385
|
+
f" {forensic_backend:20s} {fail_mark} chain verification FAILED ({n} records)"
|
|
1386
|
+
)
|
|
1387
|
+
issues.append("Forensic hash-chain integrity check failed")
|
|
1388
|
+
except ImportError as _exc:
|
|
1389
|
+
click.echo(f" {forensic_backend:20s} -- skipped ({_exc})")
|
|
1390
|
+
except Exception as _exc:
|
|
1391
|
+
click.echo(f" {forensic_backend:20s} -- skipped (error: {_exc})")
|
|
1392
|
+
else:
|
|
1393
|
+
click.echo(f" {'not configured':20s} -- memory backend (no durable chain to verify)")
|
|
1394
|
+
|
|
1330
1395
|
# ── Summary ──────────────────────────────────────────────
|
|
1331
1396
|
click.echo("\n" + "=" * 55)
|
|
1332
1397
|
if not issues:
|
{admina_framework-0.9.5 → admina_framework-0.10.1}/admina/cli/templates/docker-compose.yml.j2
RENAMED
|
@@ -7,9 +7,9 @@ services:
|
|
|
7
7
|
# ── Core: Governance Proxy ──────────────────────────────
|
|
8
8
|
{% if domains.agent_security %}
|
|
9
9
|
proxy:
|
|
10
|
-
# Pinned to
|
|
10
|
+
# Pinned to v{{ admina_version }} — change to :latest to track the rolling tag.
|
|
11
11
|
# Images are published by .github/workflows/release-docker.yml on tag push.
|
|
12
|
-
image: ghcr.io/admina-org/admina-proxy:
|
|
12
|
+
image: ghcr.io/admina-org/admina-proxy:{{ admina_version }}
|
|
13
13
|
container_name: {{ project_name }}-proxy
|
|
14
14
|
ports:
|
|
15
15
|
- "8080:8080"
|
|
@@ -40,7 +40,7 @@ services:
|
|
|
40
40
|
|
|
41
41
|
# ── Dashboard ───────────────────────────────────────────
|
|
42
42
|
dashboard:
|
|
43
|
-
image: ghcr.io/admina-org/admina-dashboard:
|
|
43
|
+
image: ghcr.io/admina-org/admina-dashboard:{{ admina_version }}
|
|
44
44
|
container_name: {{ project_name }}-dashboard
|
|
45
45
|
ports:
|
|
46
46
|
- "3000:80"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""{{ plugin_name }} — Admina {{ plugin_type }} plugin."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
{% if needs_any %}from typing import Any
|
|
5
|
+
|
|
6
|
+
{% endif %}from admina.plugins.base import {{ base_class }}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class {{ class_name }}({{ base_class }}):
|
|
10
|
+
"""{{ plugin_type.replace('_', ' ').title() }} plugin: {{ plugin_name }}.
|
|
11
|
+
|
|
12
|
+
TODO: Implement the required methods below.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "{{ plugin_name }}"
|
|
16
|
+
{%- if name_property == "framework_name" %}
|
|
17
|
+
framework_name = "{{ plugin_name }}"
|
|
18
|
+
{%- elif name_property == "protocol_name" %}
|
|
19
|
+
protocol_name = "{{ plugin_name }}"
|
|
20
|
+
{%- elif name_property == "store_name" %}
|
|
21
|
+
store_name = "{{ plugin_name }}"
|
|
22
|
+
{%- elif name_property == "provider_name" %}
|
|
23
|
+
provider_name = "{{ plugin_name }}"
|
|
24
|
+
{%- elif name_property == "channel_name" %}
|
|
25
|
+
channel_name = "{{ plugin_name }}"
|
|
26
|
+
{%- elif name_property == "supported_languages" %}
|
|
27
|
+
supported_languages = ["en"]
|
|
28
|
+
{%- endif %}
|
|
29
|
+
# Optional: accept per-plugin config from admina.yaml `plugin_config:`
|
|
30
|
+
# def __init__(self, config: dict | None = None) -> None:
|
|
31
|
+
# self._config = config or {}
|
|
32
|
+
{% if plugin_type == "model_adapter" %}
|
|
33
|
+
async def send(
|
|
34
|
+
self,
|
|
35
|
+
prompt: str,
|
|
36
|
+
context: Any = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Send a prompt to the model and return the response.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict with at least ``text`` and ``metadata`` keys.
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError("TODO: implement send()")
|
|
45
|
+
|
|
46
|
+
def supports_model(self, model_name: str) -> bool:
|
|
47
|
+
"""Return True if this adapter supports the given model."""
|
|
48
|
+
return True
|
|
49
|
+
{%- elif plugin_type == "data_connector" %}
|
|
50
|
+
async def ingest(self, source: Any, **kwargs: Any) -> dict[str, Any]:
|
|
51
|
+
"""Ingest documents from *source* into the data store."""
|
|
52
|
+
raise NotImplementedError("TODO: implement ingest()")
|
|
53
|
+
|
|
54
|
+
async def query(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
55
|
+
"""Query the data store and return ranked results."""
|
|
56
|
+
raise NotImplementedError("TODO: implement query()")
|
|
57
|
+
{%- elif plugin_type == "governance_guard" %}
|
|
58
|
+
async def inspect_request(self, request: dict) -> dict:
|
|
59
|
+
"""Inspect an inbound request."""
|
|
60
|
+
return {"action": "ALLOW", "risk_level": "LOW", "details": ""}
|
|
61
|
+
|
|
62
|
+
async def inspect_response(self, response: dict) -> dict:
|
|
63
|
+
"""Inspect an outbound response."""
|
|
64
|
+
return {"action": "ALLOW", "risk_level": "LOW", "details": ""}
|
|
65
|
+
{%- elif plugin_type == "compliance_template" %}
|
|
66
|
+
def get_requirements(self) -> list[dict]:
|
|
67
|
+
"""Return the list of requirements for this framework."""
|
|
68
|
+
raise NotImplementedError("TODO: implement get_requirements()")
|
|
69
|
+
|
|
70
|
+
def evaluate(self, governance_state: dict) -> dict:
|
|
71
|
+
"""Evaluate governance state against the requirements."""
|
|
72
|
+
raise NotImplementedError("TODO: implement evaluate()")
|
|
73
|
+
{%- elif plugin_type == "transport_adapter" %}
|
|
74
|
+
async def parse_request(self, raw_request: Any) -> Any:
|
|
75
|
+
"""Parse a protocol-specific request into a GovernanceRequest."""
|
|
76
|
+
raise NotImplementedError("TODO: implement parse_request()")
|
|
77
|
+
|
|
78
|
+
async def format_response(self, gov_response: Any, original: Any) -> Any:
|
|
79
|
+
"""Format a GovernanceResponse into the protocol wire format."""
|
|
80
|
+
raise NotImplementedError("TODO: implement format_response()")
|
|
81
|
+
|
|
82
|
+
def register_routes(self, app: Any) -> None:
|
|
83
|
+
"""Register protocol-specific HTTP routes on the FastAPI app."""
|
|
84
|
+
raise NotImplementedError("TODO: implement register_routes()")
|
|
85
|
+
{%- elif plugin_type == "forensic_store" %}
|
|
86
|
+
async def append(self, record: dict) -> str:
|
|
87
|
+
"""Persist a governance record; return a storage identifier."""
|
|
88
|
+
raise NotImplementedError("TODO: implement append()")
|
|
89
|
+
|
|
90
|
+
async def verify_chain(self, last_n: int = 0) -> dict:
|
|
91
|
+
"""Verify the integrity of the hash chain."""
|
|
92
|
+
raise NotImplementedError("TODO: implement verify_chain()")
|
|
93
|
+
{%- elif plugin_type == "auth_provider" %}
|
|
94
|
+
async def authenticate(self, request: Any) -> dict:
|
|
95
|
+
"""Authenticate a request; return ``{user_id, roles, metadata}``."""
|
|
96
|
+
raise NotImplementedError("TODO: implement authenticate()")
|
|
97
|
+
|
|
98
|
+
async def authorize(self, user: dict, action: str, resource: str = "") -> bool:
|
|
99
|
+
"""Check whether *user* may perform *action*."""
|
|
100
|
+
raise NotImplementedError("TODO: implement authorize()")
|
|
101
|
+
{%- elif plugin_type == "pii_engine" %}
|
|
102
|
+
async def detect(
|
|
103
|
+
self,
|
|
104
|
+
text: str,
|
|
105
|
+
categories: list[str] | None = None,
|
|
106
|
+
) -> list[dict]:
|
|
107
|
+
"""Detect PII entities in *text*."""
|
|
108
|
+
raise NotImplementedError("TODO: implement detect()")
|
|
109
|
+
|
|
110
|
+
async def redact(self, text: str, matches: list[dict]) -> str:
|
|
111
|
+
"""Replace detected PII spans with placeholders."""
|
|
112
|
+
raise NotImplementedError("TODO: implement redact()")
|
|
113
|
+
{%- elif plugin_type == "alert_channel" %}
|
|
114
|
+
async def send_alert(self, alert: dict) -> bool:
|
|
115
|
+
"""Send a governance alert through this channel."""
|
|
116
|
+
raise NotImplementedError("TODO: implement send_alert()")
|
|
117
|
+
{%- endif %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "{{ plugin_name }}"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Admina {{ plugin_type.replace('_', ' ') }} plugin: {{ plugin_name }}"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
# Floor pinned to the admina-framework version that generated this
|
|
12
|
+
# scaffold: the plugin contract (class-attr name, async ABC methods)
|
|
13
|
+
# requires at least that runtime.
|
|
14
|
+
"admina-framework>={{ admina_version }}",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.entry-points."admina.plugins"]
|
|
18
|
+
{{ plugin_name }} = "{{ plugin_name.replace('-', '_') }}"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=8.0"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tests for {{ plugin_name }} plugin."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from {{ plugin_name.replace('-', '_') }} import {{ class_name }}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Test{{ class_name }}:
|
|
8
|
+
"""Basic tests for {{ class_name }}."""
|
|
9
|
+
|
|
10
|
+
def test_instantiation(self) -> None:
|
|
11
|
+
plugin = {{ class_name }}()
|
|
12
|
+
assert plugin is not None
|
|
13
|
+
|
|
14
|
+
def test_name(self) -> None:
|
|
15
|
+
plugin = {{ class_name }}()
|
|
16
|
+
assert plugin.name == "{{ plugin_name }}"
|
|
17
|
+
{%- if name_property == "supported_languages" %}
|
|
18
|
+
|
|
19
|
+
def test_supported_languages(self) -> None:
|
|
20
|
+
plugin = {{ class_name }}()
|
|
21
|
+
assert "en" in plugin.supported_languages
|
|
22
|
+
{%- elif name_property != "name" %}
|
|
23
|
+
|
|
24
|
+
def test_{{ name_property }}(self) -> None:
|
|
25
|
+
plugin = {{ class_name }}()
|
|
26
|
+
assert plugin.{{ name_property }} == "{{ plugin_name }}"
|
|
27
|
+
{%- endif %}
|
|
@@ -138,6 +138,8 @@ class FirewallConfig:
|
|
|
138
138
|
|
|
139
139
|
enabled: bool = True
|
|
140
140
|
heuristic_threshold: float = 0.7
|
|
141
|
+
custom_patterns: list[dict] = field(default_factory=list)
|
|
142
|
+
disabled_categories: list[str] = field(default_factory=list)
|
|
141
143
|
|
|
142
144
|
|
|
143
145
|
@dataclass
|
|
@@ -210,7 +212,7 @@ class AdminaConfig:
|
|
|
210
212
|
otherwise falls back to ``.env`` variables.
|
|
211
213
|
"""
|
|
212
214
|
|
|
213
|
-
|
|
215
|
+
schema_version: int = 1
|
|
214
216
|
data_sovereignty: DataSovereigntyConfig = field(default_factory=DataSovereigntyConfig)
|
|
215
217
|
ai_infra: AIInfraConfig = field(default_factory=AIInfraConfig)
|
|
216
218
|
agent_security: AgentSecurityConfig = field(default_factory=AgentSecurityConfig)
|
|
@@ -221,6 +223,7 @@ class AdminaConfig:
|
|
|
221
223
|
pii_engine: str = "spacy-regex"
|
|
222
224
|
alert_channels: list[AlertChannelConfig] = field(default_factory=list)
|
|
223
225
|
plugins: list[str] = field(default_factory=list)
|
|
226
|
+
plugin_config: dict[str, Any] = field(default_factory=dict)
|
|
224
227
|
|
|
225
228
|
# Storage — populated from .env fallback when YAML absent
|
|
226
229
|
redis_url: str = "redis://localhost:6379/0"
|
|
@@ -240,6 +243,15 @@ class AdminaConfig:
|
|
|
240
243
|
# ── YAML parsing helpers ─────────────────────────────────────
|
|
241
244
|
|
|
242
245
|
|
|
246
|
+
def _parse_schema_version(data: dict) -> int:
|
|
247
|
+
"""Read ``schema_version`` (int); tolerate the legacy ``version`` key."""
|
|
248
|
+
raw = data.get("schema_version", data.get("version", 1))
|
|
249
|
+
try:
|
|
250
|
+
return int(raw)
|
|
251
|
+
except (TypeError, ValueError):
|
|
252
|
+
return 1
|
|
253
|
+
|
|
254
|
+
|
|
243
255
|
def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
|
|
244
256
|
"""Build an :class:`AdminaConfig` from a parsed YAML dict."""
|
|
245
257
|
domains = data.get("domains", {})
|
|
@@ -318,6 +330,8 @@ def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
|
|
|
318
330
|
firewall=FirewallConfig(
|
|
319
331
|
enabled=fw_raw.get("enabled", True),
|
|
320
332
|
heuristic_threshold=fw_raw.get("heuristic_threshold", 0.7),
|
|
333
|
+
custom_patterns=list(fw_raw.get("custom_patterns") or []),
|
|
334
|
+
disabled_categories=list(fw_raw.get("disabled_categories") or []),
|
|
321
335
|
),
|
|
322
336
|
loop_breaker=LoopBreakerConfig(
|
|
323
337
|
enabled=lb_raw.get("enabled", True),
|
|
@@ -359,7 +373,7 @@ def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
|
|
|
359
373
|
]
|
|
360
374
|
|
|
361
375
|
return AdminaConfig(
|
|
362
|
-
|
|
376
|
+
schema_version=_parse_schema_version(data),
|
|
363
377
|
data_sovereignty=ds,
|
|
364
378
|
ai_infra=ai,
|
|
365
379
|
agent_security=agent_sec,
|
|
@@ -370,6 +384,7 @@ def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
|
|
|
370
384
|
pii_engine=data.get("pii_engine", "spacy-regex"),
|
|
371
385
|
alert_channels=alerts,
|
|
372
386
|
plugins=data.get("plugins", []),
|
|
387
|
+
plugin_config=data.get("plugin_config", {}),
|
|
373
388
|
)
|
|
374
389
|
|
|
375
390
|
|
|
@@ -32,12 +32,52 @@ import logging
|
|
|
32
32
|
import re
|
|
33
33
|
from dataclasses import dataclass, field
|
|
34
34
|
from enum import Enum
|
|
35
|
+
from html.parser import HTMLParser
|
|
35
36
|
from pathlib import Path
|
|
36
37
|
from typing import Any, Protocol, runtime_checkable
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger("admina.ai_infra.rag")
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class _HTMLTextExtractor(HTMLParser):
|
|
43
|
+
"""Extract visible text from HTML, dropping ``<script>``/``<style>`` bodies.
|
|
44
|
+
|
|
45
|
+
Uses the stdlib HTML parser instead of regex tag-stripping: a real parser
|
|
46
|
+
handles malformed/nested tags a regex cannot (e.g. ``<scr<script>ipt>``),
|
|
47
|
+
so script/style content can't leak into the extracted text. This is a
|
|
48
|
+
best-effort ingestion extractor for RAG, not a security sanitizer — the
|
|
49
|
+
output is embedded as LLM context, never rendered as HTML.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Substring match (not equality): a malformed payload like
|
|
53
|
+
# "<scr<script>ipt>" is tokenised by HTMLParser as a single start tag
|
|
54
|
+
# named "scr<script", so an exact "== 'script'" check would miss it.
|
|
55
|
+
_SKIP_TAGS = ("script", "style")
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__(convert_charrefs=True)
|
|
59
|
+
self._parts: list[str] = []
|
|
60
|
+
self._skip_depth = 0
|
|
61
|
+
|
|
62
|
+
def _is_skip(self, tag: str) -> bool:
|
|
63
|
+
return any(s in tag for s in self._SKIP_TAGS)
|
|
64
|
+
|
|
65
|
+
def handle_starttag(self, tag: str, attrs: object) -> None:
|
|
66
|
+
if self._is_skip(tag):
|
|
67
|
+
self._skip_depth += 1
|
|
68
|
+
|
|
69
|
+
def handle_endtag(self, tag: str) -> None:
|
|
70
|
+
if self._is_skip(tag) and self._skip_depth > 0:
|
|
71
|
+
self._skip_depth -= 1
|
|
72
|
+
|
|
73
|
+
def handle_data(self, data: str) -> None:
|
|
74
|
+
if self._skip_depth == 0:
|
|
75
|
+
self._parts.append(data)
|
|
76
|
+
|
|
77
|
+
def get_text(self) -> str:
|
|
78
|
+
return " ".join(self._parts)
|
|
79
|
+
|
|
80
|
+
|
|
41
81
|
# ── Document types ───────────────────────────────────────────
|
|
42
82
|
|
|
43
83
|
|
|
@@ -131,12 +171,15 @@ def parse_plain_text(content: str) -> str:
|
|
|
131
171
|
|
|
132
172
|
|
|
133
173
|
def parse_html(content: str) -> str:
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
174
|
+
"""Extract plain text from HTML, dropping script/style content.
|
|
175
|
+
|
|
176
|
+
Uses the stdlib HTML parser (see :class:`_HTMLTextExtractor`) rather than
|
|
177
|
+
regex tag-stripping, which a malformed-tag payload can bypass.
|
|
178
|
+
"""
|
|
179
|
+
extractor = _HTMLTextExtractor()
|
|
180
|
+
extractor.feed(content)
|
|
181
|
+
text = extractor.get_text()
|
|
182
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
140
183
|
|
|
141
184
|
|
|
142
185
|
def parse_csv(content: str) -> str:
|
|
@@ -19,9 +19,32 @@ Automated risk classification, gap analysis, and compliance reporting.
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
from datetime import UTC, datetime
|
|
22
|
+
from typing import Any
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger("admina.eu_ai_act")
|
|
24
25
|
|
|
26
|
+
|
|
27
|
+
def _coerce_checks(value: Any, n: int) -> list[bool]:
|
|
28
|
+
"""Normalise a declared-compliance value to exactly *n* booleans.
|
|
29
|
+
|
|
30
|
+
bool → that value for the first check, the rest unmet (a single claim
|
|
31
|
+
does not satisfy all N checks); list → padded with False / truncated to
|
|
32
|
+
n; None → all unmet. Prevents a bool/short input from being zip-truncated
|
|
33
|
+
into a falsely high score.
|
|
34
|
+
"""
|
|
35
|
+
if value is None:
|
|
36
|
+
return [False] * n
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
declared = [value]
|
|
39
|
+
elif isinstance(value, list):
|
|
40
|
+
declared = [bool(v) for v in value]
|
|
41
|
+
else:
|
|
42
|
+
declared = [False]
|
|
43
|
+
declared = declared[:n]
|
|
44
|
+
declared += [False] * (n - len(declared))
|
|
45
|
+
return declared
|
|
46
|
+
|
|
47
|
+
|
|
25
48
|
# ── EU AI Act application timeline (Art. 113, Regulation 2024/1689) ─────────
|
|
26
49
|
# Reflects the "AI Act Omnibus" agreement reached by Council and Parliament
|
|
27
50
|
# on 7 May 2026 (Omnibus VII), which postponed several high-risk deadlines
|
|
@@ -288,8 +311,9 @@ class EUAIActCompliance:
|
|
|
288
311
|
passed_checks = 0
|
|
289
312
|
|
|
290
313
|
for req_key, req_info in HIGH_RISK_REQUIREMENTS.items():
|
|
291
|
-
|
|
292
|
-
|
|
314
|
+
n = len(req_info["checks"])
|
|
315
|
+
declared = _coerce_checks(current_compliance.get(req_key), n)
|
|
316
|
+
for i, (check_desc, is_met) in enumerate(zip(req_info["checks"], declared)):
|
|
293
317
|
total_checks += 1
|
|
294
318
|
if is_met:
|
|
295
319
|
passed_checks += 1
|