admina-framework 0.12.1__tar.gz → 0.13.0__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.
Files changed (306) hide show
  1. admina_framework-0.13.0/PKG-INFO +1996 -0
  2. admina_framework-0.13.0/README.md +1912 -0
  3. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/__init__.py +1 -1
  4. admina_framework-0.13.0/admina/cli/forensic.py +197 -0
  5. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/main.py +44 -9
  6. admina_framework-0.13.0/admina/cli/redteam.py +170 -0
  7. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/admina.yaml.j2 +6 -2
  8. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/docker-compose.yml.j2 +8 -1
  9. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/plugin.py.j2 +5 -0
  10. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/core/config.py +370 -7
  11. admina_framework-0.13.0/admina/core/config_schema.py +325 -0
  12. admina_framework-0.13.0/admina/core/exception_log.py +44 -0
  13. admina_framework-0.13.0/admina/core/jcs.py +150 -0
  14. admina_framework-0.13.0/admina/core/offline.py +69 -0
  15. admina_framework-0.13.0/admina/core/secretfile.py +109 -0
  16. admina_framework-0.13.0/admina/core/trace_context.py +82 -0
  17. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/core/types.py +7 -0
  18. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/dashboard/static/index.html +28 -11
  19. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/agent_security/egress.py +66 -0
  20. admina_framework-0.13.0/admina/domains/agent_security/firewall.py +1096 -0
  21. admina_framework-0.13.0/admina/domains/agent_security/pattern_packs.py +466 -0
  22. admina_framework-0.13.0/admina/domains/agent_security/pattern_timing.py +301 -0
  23. admina_framework-0.13.0/admina/domains/agent_security/ruleset.py +269 -0
  24. admina_framework-0.13.0/admina/domains/agent_security/scan_policy.py +406 -0
  25. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/__init__.py +10 -0
  26. admina_framework-0.13.0/admina/domains/compliance/ai_act_terms.py +386 -0
  27. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/eu_ai_act.py +79 -21
  28. admina_framework-0.13.0/admina/domains/compliance/forensic.py +1195 -0
  29. admina_framework-0.13.0/admina/domains/compliance/forensic_files.py +222 -0
  30. admina_framework-0.13.0/admina/domains/compliance/forensic_integrity.py +372 -0
  31. admina_framework-0.13.0/admina/domains/compliance/oisg_evidence.py +435 -0
  32. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/otel.py +43 -1
  33. admina_framework-0.13.0/admina/domains/compliance/schemas/oisg-evidence.schema.json +63 -0
  34. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/data_sovereignty/classification.py +43 -8
  35. admina_framework-0.13.0/admina/domains/data_sovereignty/email_matching.py +74 -0
  36. admina_framework-0.13.0/admina/domains/data_sovereignty/iban.py +113 -0
  37. admina_framework-0.13.0/admina/domains/data_sovereignty/masking.py +253 -0
  38. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/data_sovereignty/pii.py +75 -25
  39. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/governance.py +198 -25
  40. admina_framework-0.13.0/admina/engines/__init__.py +798 -0
  41. admina_framework-0.13.0/admina/engines/pii_plugins.py +294 -0
  42. admina_framework-0.13.0/admina/engines/presidio.py +348 -0
  43. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/base.py +40 -0
  44. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/forensic/filesystem.py +7 -5
  45. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/guards/guardrailsai_guard.py +8 -1
  46. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/pii/spacy_regex.py +8 -7
  47. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/api/dashboard.py +180 -29
  48. admina_framework-0.13.0/admina/proxy/api/gateway.py +1549 -0
  49. admina_framework-0.13.0/admina/proxy/api/integration.py +404 -0
  50. admina_framework-0.13.0/admina/proxy/body_limit.py +132 -0
  51. admina_framework-0.13.0/admina/proxy/config.py +543 -0
  52. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/dashboard_session.py +66 -0
  53. admina_framework-0.13.0/admina/proxy/decisions.py +188 -0
  54. admina_framework-0.13.0/admina/proxy/engine_report.py +83 -0
  55. admina_framework-0.13.0/admina/proxy/env_check.py +164 -0
  56. admina_framework-0.13.0/admina/proxy/forensic_backend.py +233 -0
  57. admina_framework-0.13.0/admina/proxy/forensic_probe.py +142 -0
  58. admina_framework-0.13.0/admina/proxy/gateway_body.py +152 -0
  59. admina_framework-0.13.0/admina/proxy/gateway_correlation.py +177 -0
  60. admina_framework-0.13.0/admina/proxy/gateway_outcome.py +292 -0
  61. admina_framework-0.13.0/admina/proxy/gateway_response_scan.py +149 -0
  62. admina_framework-0.13.0/admina/proxy/gateway_scan.py +168 -0
  63. admina_framework-0.13.0/admina/proxy/gateway_transport.py +91 -0
  64. admina_framework-0.13.0/admina/proxy/gateway_upstreams.py +272 -0
  65. admina_framework-0.13.0/admina/proxy/log_format.py +72 -0
  66. admina_framework-0.13.0/admina/proxy/loop_lag.py +104 -0
  67. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/main.py +775 -330
  68. admina_framework-0.13.0/admina/proxy/pipeline_executor.py +150 -0
  69. admina_framework-0.13.0/admina/proxy/request_metrics.py +209 -0
  70. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/state.py +65 -4
  71. admina_framework-0.13.0/admina/proxy/surfaces.py +78 -0
  72. admina_framework-0.13.0/admina/redteam/__init__.py +252 -0
  73. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/baselines/baseline.json +1 -1
  74. admina_framework-0.13.0/admina/redteam/corpora.py +170 -0
  75. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/detectors.py +64 -10
  76. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/gate.py +10 -0
  77. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/report.py +13 -1
  78. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/__init__.py +2 -0
  79. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/governed_agent.py +17 -5
  80. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/governed_model.py +11 -8
  81. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/streaming.py +25 -2
  82. admina_framework-0.13.0/admina_framework.egg-info/PKG-INFO +1996 -0
  83. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina_framework.egg-info/SOURCES.txt +119 -0
  84. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina_framework.egg-info/requires.txt +9 -1
  85. {admina_framework-0.12.1 → admina_framework-0.13.0}/pyproject.toml +44 -20
  86. admina_framework-0.13.0/tests/test_admina_config_path.py +264 -0
  87. admina_framework-0.13.0/tests/test_ai_act_terms.py +121 -0
  88. admina_framework-0.13.0/tests/test_audit_api_source.py +225 -0
  89. admina_framework-0.13.0/tests/test_auth_middleware_errors.py +155 -0
  90. admina_framework-0.13.0/tests/test_banner_engine_info.py +146 -0
  91. admina_framework-0.13.0/tests/test_bus_event_metadata.py +330 -0
  92. admina_framework-0.13.0/tests/test_canary_no_content.py +486 -0
  93. admina_framework-0.13.0/tests/test_check_versions.py +153 -0
  94. admina_framework-0.13.0/tests/test_cli_forensic_export.py +229 -0
  95. admina_framework-0.13.0/tests/test_cli_redteam.py +276 -0
  96. admina_framework-0.13.0/tests/test_config_schema_validation.py +481 -0
  97. admina_framework-0.13.0/tests/test_dashboard_container.py +178 -0
  98. admina_framework-0.13.0/tests/test_data_classifier_restricted.py +88 -0
  99. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_domains_governance.py +3 -3
  100. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_egress_surfaces.py +202 -1
  101. admina_framework-0.13.0/tests/test_enabled_surfaces.py +277 -0
  102. admina_framework-0.13.0/tests/test_engine_effective.py +212 -0
  103. admina_framework-0.13.0/tests/test_engine_startup_errors.py +294 -0
  104. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_engines.py +73 -17
  105. admina_framework-0.13.0/tests/test_entrypoint_script.py +96 -0
  106. admina_framework-0.13.0/tests/test_event_loop_lag_metric.py +142 -0
  107. admina_framework-0.13.0/tests/test_firewall_heuristic_config.py +320 -0
  108. admina_framework-0.13.0/tests/test_firewall_it_baseline.py +407 -0
  109. admina_framework-0.13.0/tests/test_firewall_it_benign.py +123 -0
  110. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_firewall_parity.py +63 -4
  111. admina_framework-0.13.0/tests/test_firewall_pattern_ids.py +278 -0
  112. admina_framework-0.13.0/tests/test_firewall_pattern_packs.py +554 -0
  113. admina_framework-0.13.0/tests/test_firewall_pattern_timing.py +721 -0
  114. admina_framework-0.13.0/tests/test_forensic_atomic_write.py +192 -0
  115. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_forensic_blackbox.py +120 -32
  116. admina_framework-0.13.0/tests/test_forensic_config_coherence.py +157 -0
  117. admina_framework-0.13.0/tests/test_forensic_fail_mode.py +533 -0
  118. admina_framework-0.13.0/tests/test_forensic_gateway_records.py +551 -0
  119. admina_framework-0.13.0/tests/test_forensic_incremental_verify.py +234 -0
  120. admina_framework-0.13.0/tests/test_forensic_integrity.py +383 -0
  121. admina_framework-0.13.0/tests/test_forensic_rebuilt_and_duplicates.py +126 -0
  122. admina_framework-0.13.0/tests/test_forensic_record_signature.py +360 -0
  123. admina_framework-0.13.0/tests/test_forensic_restart.py +193 -0
  124. admina_framework-0.13.0/tests/test_forensic_s3_reads.py +251 -0
  125. admina_framework-0.13.0/tests/test_forensic_volume.py +84 -0
  126. admina_framework-0.13.0/tests/test_gateway_block_status.py +189 -0
  127. admina_framework-0.13.0/tests/test_gateway_correlation.py +435 -0
  128. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_gateway_endpoints.py +22 -6
  129. admina_framework-0.13.0/tests/test_gateway_errors.py +370 -0
  130. admina_framework-0.13.0/tests/test_gateway_forward_body.py +367 -0
  131. admina_framework-0.13.0/tests/test_gateway_governed_stream.py +735 -0
  132. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_gateway_guard_fail_mode.py +11 -1
  133. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_gateway_helpers.py +22 -64
  134. admina_framework-0.13.0/tests/test_gateway_latency_bench.py +199 -0
  135. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_gateway_mount.py +23 -1
  136. admina_framework-0.13.0/tests/test_gateway_outcome_headers.py +424 -0
  137. admina_framework-0.13.0/tests/test_gateway_passthrough.py +378 -0
  138. admina_framework-0.13.0/tests/test_gateway_pii_contract.py +195 -0
  139. admina_framework-0.13.0/tests/test_gateway_ruleset_endpoint.py +359 -0
  140. admina_framework-0.13.0/tests/test_gateway_scan_coverage.py +587 -0
  141. admina_framework-0.13.0/tests/test_gateway_upstream_auth.py +659 -0
  142. admina_framework-0.13.0/tests/test_gateway_upstream_routes.py +614 -0
  143. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_governance_pipeline.py +35 -3
  144. admina_framework-0.13.0/tests/test_health_fields.py +268 -0
  145. admina_framework-0.13.0/tests/test_health_forensic_probe.py +303 -0
  146. admina_framework-0.13.0/tests/test_iban_it.py +224 -0
  147. admina_framework-0.13.0/tests/test_jcs.py +256 -0
  148. admina_framework-0.13.0/tests/test_lazy_imports.py +247 -0
  149. admina_framework-0.13.0/tests/test_log_format.py +116 -0
  150. admina_framework-0.13.0/tests/test_metrics_docs_auth.py +112 -0
  151. admina_framework-0.13.0/tests/test_metrics_gateway.py +410 -0
  152. admina_framework-0.13.0/tests/test_model_allowlist_post.py +153 -0
  153. admina_framework-0.13.0/tests/test_offline_guarantee.py +222 -0
  154. admina_framework-0.13.0/tests/test_oisg_evidence.py +448 -0
  155. admina_framework-0.13.0/tests/test_otel_enabled_flag.py +148 -0
  156. admina_framework-0.13.0/tests/test_pattern_probe.py +247 -0
  157. admina_framework-0.13.0/tests/test_pii_email_matching.py +160 -0
  158. admina_framework-0.13.0/tests/test_pii_mask_style.py +375 -0
  159. admina_framework-0.13.0/tests/test_pii_plugin_engines.py +255 -0
  160. admina_framework-0.13.0/tests/test_pipeline_executor.py +732 -0
  161. admina_framework-0.13.0/tests/test_presidio_engine.py +217 -0
  162. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_dashboard_session.py +228 -3
  163. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_guard_fail_mode.py +4 -2
  164. admina_framework-0.13.0/tests/test_proxy_minimal_extra.py +219 -0
  165. admina_framework-0.13.0/tests/test_query_api_key_deprecation.py +97 -0
  166. admina_framework-0.13.0/tests/test_record_decision.py +280 -0
  167. admina_framework-0.13.0/tests/test_redaction_values_only.py +422 -0
  168. admina_framework-0.13.0/tests/test_redteam_external_corpora.py +505 -0
  169. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_redteam_lib.py +18 -0
  170. admina_framework-0.13.0/tests/test_release_workflows.py +370 -0
  171. admina_framework-0.13.0/tests/test_request_sha256.py +141 -0
  172. admina_framework-0.13.0/tests/test_request_size_cap.py +342 -0
  173. admina_framework-0.13.0/tests/test_response_scan_option.py +279 -0
  174. admina_framework-0.13.0/tests/test_ruleset_parity.py +149 -0
  175. admina_framework-0.13.0/tests/test_ruleset_sha256.py +519 -0
  176. admina_framework-0.13.0/tests/test_scan_depth.py +176 -0
  177. admina_framework-0.13.0/tests/test_scan_policy.py +611 -0
  178. admina_framework-0.13.0/tests/test_secret_files.py +315 -0
  179. admina_framework-0.13.0/tests/test_unknown_admina_env.py +245 -0
  180. admina_framework-0.12.1/PKG-INFO +0 -745
  181. admina_framework-0.12.1/README.md +0 -668
  182. admina_framework-0.12.1/admina/domains/agent_security/firewall.py +0 -634
  183. admina_framework-0.12.1/admina/domains/compliance/forensic.py +0 -537
  184. admina_framework-0.12.1/admina/engines/__init__.py +0 -475
  185. admina_framework-0.12.1/admina/engines/presidio.py +0 -181
  186. admina_framework-0.12.1/admina/proxy/api/gateway.py +0 -404
  187. admina_framework-0.12.1/admina/proxy/api/integration.py +0 -228
  188. admina_framework-0.12.1/admina/proxy/config.py +0 -249
  189. admina_framework-0.12.1/admina/redteam/__init__.py +0 -72
  190. admina_framework-0.12.1/admina/redteam/corpora.py +0 -52
  191. admina_framework-0.12.1/admina_framework.egg-info/PKG-INFO +0 -745
  192. admina_framework-0.12.1/tests/test_presidio_engine.py +0 -69
  193. {admina_framework-0.12.1 → admina_framework-0.13.0}/LICENSE +0 -0
  194. {admina_framework-0.12.1 → admina_framework-0.13.0}/NOTICE +0 -0
  195. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/__init__.py +0 -0
  196. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/commands/__init__.py +0 -0
  197. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/env.j2 +0 -0
  198. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/main.py.j2 +0 -0
  199. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/plugin_pyproject.toml.j2 +0 -0
  200. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/plugin_readme.md.j2 +0 -0
  201. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/cli/templates/plugin_test.py.j2 +0 -0
  202. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/core/__init__.py +0 -0
  203. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/core/event_bus.py +0 -0
  204. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/core/secrets.py +0 -0
  205. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/dashboard/__init__.py +0 -0
  206. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/dashboard/static/heimdall.png +0 -0
  207. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
  208. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/__init__.py +0 -0
  209. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/agent_security/__init__.py +0 -0
  210. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/agent_security/coordination.py +0 -0
  211. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/agent_security/fingerprint.py +0 -0
  212. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/agent_security/loop_breaker.py +0 -0
  213. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/ai_infra/__init__.py +0 -0
  214. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/ai_infra/llm_engine.py +0 -0
  215. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/ai_infra/rag.py +0 -0
  216. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/ai_infra/webui.py +0 -0
  217. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/cross_regulation.py +0 -0
  218. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/gdpr.py +0 -0
  219. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/nis2.py +0 -0
  220. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/compliance/oisg.py +0 -0
  221. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/data_sovereignty/__init__.py +0 -0
  222. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/domains/data_sovereignty/residency.py +0 -0
  223. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/__init__.py +0 -0
  224. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/_engines.py +0 -0
  225. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/cheshirecat/__init__.py +0 -0
  226. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
  227. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/crewai/__init__.py +0 -0
  228. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/crewai/callbacks.py +0 -0
  229. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/langchain/__init__.py +0 -0
  230. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/langchain/callbacks.py +0 -0
  231. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/n8n/__init__.py +0 -0
  232. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/integrations/openclaw/__init__.py +0 -0
  233. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/__init__.py +0 -0
  234. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/__init__.py +0 -0
  235. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/__init__.py +0 -0
  236. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/_streaming.py +0 -0
  237. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/anthropic.py +0 -0
  238. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/bedrock.py +0 -0
  239. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/gemini.py +0 -0
  240. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/mistral.py +0 -0
  241. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/ollama.py +0 -0
  242. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/openai.py +0 -0
  243. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/adapters/vllm.py +0 -0
  244. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/alerts/__init__.py +0 -0
  245. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/alerts/log.py +0 -0
  246. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/alerts/webhook.py +0 -0
  247. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/auth/__init__.py +0 -0
  248. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/auth/apikey.py +0 -0
  249. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/compliance/__init__.py +0 -0
  250. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/compliance/eu_ai_act.py +0 -0
  251. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/connectors/__init__.py +0 -0
  252. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/connectors/chromadb.py +0 -0
  253. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/connectors/filesystem.py +0 -0
  254. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/forensic/__init__.py +0 -0
  255. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/guards/__init__.py +0 -0
  256. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/pii/__init__.py +0 -0
  257. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/transports/__init__.py +0 -0
  258. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/transports/http_rest.py +0 -0
  259. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/builtin/transports/mcp.py +0 -0
  260. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/plugins/registry.py +0 -0
  261. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/__init__.py +0 -0
  262. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/api/__init__.py +0 -0
  263. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/engine_bridge.py +0 -0
  264. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/governance.py +0 -0
  265. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/proxy/multi_upstream.py +0 -0
  266. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/py.typed +0 -0
  267. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/corpora/SHA256SUMS +0 -0
  268. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/corpora/coordination.jsonl +0 -0
  269. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/corpora/injection.jsonl +0 -0
  270. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/corpora/loop.jsonl +0 -0
  271. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/corpora/pii.jsonl +0 -0
  272. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/metrics.py +0 -0
  273. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/redteam/runner.py +0 -0
  274. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/_compat.py +0 -0
  275. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/compliance_kit.py +0 -0
  276. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/errors.py +0 -0
  277. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/governed_data.py +0 -0
  278. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina/sdk/retry.py +0 -0
  279. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina_framework.egg-info/dependency_links.txt +0 -0
  280. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina_framework.egg-info/entry_points.txt +0 -0
  281. {admina_framework-0.12.1 → admina_framework-0.13.0}/admina_framework.egg-info/top_level.txt +0 -0
  282. {admina_framework-0.12.1 → admina_framework-0.13.0}/setup.cfg +0 -0
  283. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_benchmark_14us.py +0 -0
  284. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_coordination.py +0 -0
  285. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_coordination_cli.py +0 -0
  286. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_coordination_corpus.py +0 -0
  287. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_coordination_wiring.py +0 -0
  288. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_core_config.py +0 -0
  289. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_domains.py +0 -0
  290. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_egress_analyze.py +0 -0
  291. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_egress_cli.py +0 -0
  292. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_egress_pipeline.py +0 -0
  293. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_egress_policy.py +0 -0
  294. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_error_disclosure.py +0 -0
  295. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_fingerprint.py +0 -0
  296. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_gateway_config.py +0 -0
  297. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_guard_fail_mode.py +0 -0
  298. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_mcp_transport.py +0 -0
  299. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_forensic_would_action.py +0 -0
  300. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_governance_mode_config.py +0 -0
  301. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_mcp_response_pii.py +0 -0
  302. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_metrics_endpoint.py +0 -0
  303. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_plugins.py +0 -0
  304. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_proxy_security.py +0 -0
  305. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_redteam_efficacy.py +0 -0
  306. {admina_framework-0.12.1 → admina_framework-0.13.0}/tests/test_version.py +0 -0
@@ -0,0 +1,1996 @@
1
+ Metadata-Version: 2.4
2
+ Name: admina-framework
3
+ Version: 0.13.0
4
+ Summary: Admina — governed AI development framework
5
+ Author-email: Stefano Noferi <info@admina.org>
6
+ Maintainer-email: Stefano Noferi <info@admina.org>
7
+ License-Expression: Apache-2.0
8
+ Project-URL: Homepage, https://admina.org
9
+ Project-URL: Repository, https://github.com/admina-org/admina
10
+ Project-URL: Documentation, https://github.com/admina-org/admina#readme
11
+ Project-URL: Issues, https://github.com/admina-org/admina/issues
12
+ Project-URL: Changelog, https://github.com/admina-org/admina/blob/main/CHANGELOG.md
13
+ Project-URL: Security, https://github.com/admina-org/admina/blob/main/SECURITY.md
14
+ Keywords: ai,governance,compliance,eu-ai-act,pii,llm,mcp,proxy,agent-security,injection-firewall,sdk
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Rust
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Requires-Python: >=3.11
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ License-File: NOTICE
30
+ Requires-Dist: pyyaml<7,>=6.0
31
+ Requires-Dist: click<9,>=8.1
32
+ Requires-Dist: jinja2<4,>=3.1
33
+ Requires-Dist: cryptography<51,>=41.0
34
+ Provides-Extra: proxy
35
+ Requires-Dist: fastapi<1,>=0.104; extra == "proxy"
36
+ Requires-Dist: uvicorn[standard]<1,>=0.24; extra == "proxy"
37
+ Requires-Dist: httpx<1,>=0.25; extra == "proxy"
38
+ Requires-Dist: pydantic<3,>=2.5; extra == "proxy"
39
+ Requires-Dist: pydantic-settings<3,>=2.3; extra == "proxy"
40
+ Requires-Dist: python-dotenv<2,>=1.0; extra == "proxy"
41
+ Requires-Dist: redis<9,>=5.0; extra == "proxy"
42
+ Requires-Dist: boto3<2,>=1.34; extra == "proxy"
43
+ Requires-Dist: clickhouse-connect<2,>=0.7; extra == "proxy"
44
+ Requires-Dist: typer<1,>=0.9; extra == "proxy"
45
+ Requires-Dist: numpy<2,>=1.24; extra == "proxy"
46
+ Requires-Dist: scikit-learn<2,>=1.3; extra == "proxy"
47
+ Provides-Extra: proxy-minimal
48
+ Requires-Dist: fastapi<1,>=0.104; extra == "proxy-minimal"
49
+ Requires-Dist: uvicorn[standard]<1,>=0.24; extra == "proxy-minimal"
50
+ Requires-Dist: httpx<1,>=0.25; extra == "proxy-minimal"
51
+ Requires-Dist: pydantic<3,>=2.5; extra == "proxy-minimal"
52
+ Requires-Dist: pydantic-settings<3,>=2.3; extra == "proxy-minimal"
53
+ Requires-Dist: python-dotenv<2,>=1.0; extra == "proxy-minimal"
54
+ Provides-Extra: nlp
55
+ Requires-Dist: spacy<4,>=3.8; extra == "nlp"
56
+ Provides-Extra: telemetry
57
+ Requires-Dist: opentelemetry-api<2,>=1.20; extra == "telemetry"
58
+ Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "telemetry"
59
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2,>=1.20; extra == "telemetry"
60
+ Requires-Dist: opentelemetry-instrumentation-fastapi<1,>=0.40b0; extra == "telemetry"
61
+ Provides-Extra: rust
62
+ Requires-Dist: admina-core<0.14,>=0.13.0; extra == "rust"
63
+ Provides-Extra: full
64
+ Requires-Dist: admina-framework[nlp,proxy,telemetry]; extra == "full"
65
+ Provides-Extra: openai
66
+ Requires-Dist: openai<4,>=1.0; extra == "openai"
67
+ Provides-Extra: ollama
68
+ Requires-Dist: ollama<1,>=0.3; extra == "ollama"
69
+ Provides-Extra: anthropic
70
+ Requires-Dist: anthropic<2,>=0.39; extra == "anthropic"
71
+ Provides-Extra: mistral
72
+ Requires-Dist: mistralai<3,>=1.0; extra == "mistral"
73
+ Provides-Extra: gemini
74
+ Requires-Dist: google-genai<3,>=1.0; extra == "gemini"
75
+ Provides-Extra: bedrock
76
+ Requires-Dist: boto3<2,>=1.34; extra == "bedrock"
77
+ Provides-Extra: adapters
78
+ Requires-Dist: admina-framework[anthropic,bedrock,gemini,mistral,ollama,openai]; extra == "adapters"
79
+ Provides-Extra: presidio
80
+ Requires-Dist: presidio-analyzer<3,>=2.2; extra == "presidio"
81
+ Provides-Extra: all
82
+ Requires-Dist: admina-framework[adapters,nlp,presidio,proxy,telemetry]; extra == "all"
83
+ Dynamic: license-file
84
+
85
+ <!--
86
+ <p align="center">
87
+ <img src="https://raw.githubusercontent.com/admina-org/admina/main/resources/banner.png" alt="Admina — Governed AI by Default" width="100%">
88
+ </p>
89
+ -->
90
+
91
+ <p align="center">
92
+ <strong>Install once, get governed AI.</strong><br>
93
+ <em>PII redacted · Injections blocked · Loops broken · Actions audited · EU AI Act tracked</em>
94
+ </p>
95
+
96
+ <p align="center">
97
+ <a href="https://pypi.org/project/admina-framework/"><img src="https://img.shields.io/pypi/v/admina-framework?style=flat-square&color=32CD32" alt="PyPI version"></a>
98
+ &nbsp;<a href="https://github.com/admina-org/admina/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-32CD32?style=flat-square" alt="License"></a>
99
+ &nbsp;<img src="https://img.shields.io/badge/python-3.11%2B-32CD32?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
100
+ &nbsp;<img src="https://img.shields.io/github/last-commit/admina-org/admina?style=flat-square" alt="Last commit">
101
+ </p>
102
+
103
+ <p align="center">
104
+ <a href="https://github.com/admina-org/admina/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/admina-org/admina/ci.yml?style=flat-square&label=CI&logo=githubactions&logoColor=white" alt="CI"></a>
105
+ &nbsp;<a href="https://github.com/admina-org/admina/actions/workflows/release.yml"><img src="https://img.shields.io/github/actions/workflow/status/admina-org/admina/release.yml?style=flat-square&label=release" alt="Release"></a>
106
+ &nbsp;<a href="https://github.com/admina-org/admina/actions/workflows/security.yml"><img src="https://img.shields.io/github/actions/workflow/status/admina-org/admina/security.yml?style=flat-square&label=security%20scan&logo=shield&logoColor=white" alt="Security scan"></a>
107
+ &nbsp;<a href="https://pypi.org/project/admina-framework/"><img src="https://img.shields.io/pypi/dm/admina-framework?style=flat-square&label=downloads" alt="PyPI downloads"></a>
108
+ </p>
109
+
110
+ <p align="center">
111
+ <a href="https://deepwiki.com/admina-org/admina"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
112
+ &nbsp;<a href="https://admina.org/docs"><img src="https://img.shields.io/badge/docs-admina.org-blue?style=flat-square" alt="Docs"></a>
113
+ </p>
114
+
115
+ <p align="center">
116
+ <a href="#quick-start"><img src="https://img.shields.io/badge/⚡%20Quick%20Start-2%20min-32CD32?style=for-the-badge" alt="Quick Start" height="40"></a>
117
+ &nbsp;<a href="#see-it-in-action"><img src="https://img.shields.io/badge/▶%20Live%20Demo-Dashboard-3B82F6?style=for-the-badge" alt="Live Demo" height="40"></a>
118
+ &nbsp;<a href="https://admina.org/docs"><img src="https://img.shields.io/badge/📖%20Read%20the%20Docs-admina.org-6C757D?style=for-the-badge" alt="Docs" height="40"></a>
119
+ &nbsp;<a href="https://deepwiki.com/admina-org/admina"><img src="https://img.shields.io/badge/🤖%20Ask%20DeepWiki-AI%20wiki-7C3AED?style=for-the-badge" alt="DeepWiki" height="40"></a>
120
+ </p>
121
+
122
+ ---
123
+
124
+ ## See it in action
125
+
126
+ **Scaffold a project and boot the governed proxy + dashboard — no Docker:**
127
+
128
+ <p align="center">
129
+ <img src="https://raw.githubusercontent.com/admina-org/admina/main/resources/admina-init-dev.gif" alt="admina init → admina dev → Ready on localhost:3000" width="900">
130
+ </p>
131
+
132
+ **Wrap any model in a few lines — PII is stripped before the model ever sees it:**
133
+
134
+ <p align="center">
135
+ <img src="https://raw.githubusercontent.com/admina-org/admina/main/resources/sdk-3lines.gif" alt="GovernedModel redacts PERSON, EMAIL and credit card before the LLM call" width="900">
136
+ </p>
137
+
138
+ **The governance dashboard, before and after simulated traffic — Admina Score 40 → 60:**
139
+
140
+ <table>
141
+ <tr>
142
+ <td align="center" width="50%">
143
+ <img src="https://raw.githubusercontent.com/admina-org/admina/main/resources/dashboard-firstuser.png" alt="Dashboard at first boot — score 40/100" width="100%"><br>
144
+ <em>First boot — Admina Score <strong>40/100</strong></em>
145
+ </td>
146
+ <td align="center" width="50%">
147
+ <img src="https://raw.githubusercontent.com/admina-org/admina/main/resources/dashboard-traffic.png" alt="Dashboard after simulated traffic — score 60/100" width="100%"><br>
148
+ <em>After <code>python scripts/simulate.py</code> — <strong>60/100</strong></em>
149
+ </td>
150
+ </tr>
151
+ </table>
152
+
153
+ ---
154
+
155
+ ## Why Admina?
156
+
157
+ | | Plain LLM / RAG app | **With Admina** |
158
+ | :----------------------------- | :----------------------------------- | :----------------------------------------------------------------------- |
159
+ | PII in prompts/responses | leaks unless you build redaction | **Redacted by default** — email, SSN, IBAN, phone, IP, names |
160
+ | Prompt injections | reach the model | **Screened at the proxy** — a heuristic signal, not a guarantee: regex patterns (44 on the Python engine, 15 on Rust) + scoring |
161
+ | Agent tool calls | unaudited | **Validated pre-action + logged post-action** (forensic chain) |
162
+ | Loop / runaway agents | burn tokens / budget | **Broken** — TF-IDF cosine similarity over the action stream |
163
+ | EU AI Act readiness | manual | **Gap analysis + risk classification** built-in |
164
+ | Audit trail | logs you hope nobody deletes | **SHA-256 hash chain** — tamper-evident by design |
165
+ | Adding governance to existing code | rewrite the call sites | **Zero code changes** via proxy, or 3 lines via SDK |
166
+ | Performance overhead | unknown | **Measured** — engine microbenchmarks and a gateway benchmark to run on your hardware ([Performance](#performance--hybrid-python--rust-engine)) |
167
+ | License | varies | **Apache 2.0**, open core |
168
+
169
+ > Admina is **decision-support and defense-in-depth**, not legal advice. See [Compliance scope](#compliance-scope) for the full disclaimer and limitations.
170
+
171
+ ---
172
+
173
+ ## 30-second example
174
+
175
+ ```python
176
+ from admina import GovernedModel, GovernedData, GovernedAgent, ComplianceKit
177
+ from admina.plugins.builtin.adapters.ollama import OllamaAdapter
178
+ from admina.plugins.builtin.connectors.chromadb import ChromaDBConnector
179
+
180
+ # Every call is governed: PII redacted, injections blocked, audited
181
+ adapter = OllamaAdapter(host="http://localhost:11434")
182
+ model = GovernedModel(model_name="llama3.1:8b", adapter=adapter)
183
+ response = await model.ask("Summarize this document")
184
+
185
+ # Data governance: residency enforcement, PII classification
186
+ connector = ChromaDBConnector(host="localhost", port=8000)
187
+ data = GovernedData(connector=connector, residency_zone="eu")
188
+ await data.ingest(documents)
189
+
190
+ # Agent governance: validate every tool call before execution
191
+ async def my_upstream(method, params, **kw): ... # your MCP/HTTP client
192
+ agent = GovernedAgent(upstream=my_upstream)
193
+ result = await agent.call("tools/call", {"name": "read_file", "arguments": {}})
194
+
195
+ # Compliance: EU AI Act gap analysis and risk classification
196
+ kit = ComplianceKit()
197
+ report = kit.gap_analysis(risk_category="high", current_compliance={...})
198
+ ```
199
+
200
+ ## Quick Start
201
+
202
+ ### Install from PyPI
203
+
204
+ ```bash
205
+ # Recommended for new users: SDK + proxy + dashboard.
206
+ # Lets you run `admina dev` and see the dashboard out of the box.
207
+ pip install "admina-framework[proxy]"
208
+
209
+ # Everything (proxy + NLP + telemetry). Use this if you also want
210
+ # spaCy-based NER for PII detection or OpenTelemetry export.
211
+ pip install "admina-framework[full]"
212
+ python -m spacy download en_core_web_sm # for [full] only
213
+
214
+ # Model-adapter provider SDKs (per-provider extras):
215
+ pip install "admina-framework[openai]" # openai>=1.0
216
+ pip install "admina-framework[ollama]" # ollama>=0.3
217
+ pip install "admina-framework[anthropic]" # anthropic>=0.39
218
+ pip install "admina-framework[mistral]" # mistralai>=1.0
219
+ pip install "admina-framework[gemini]" # google-genai>=1.0
220
+ pip install "admina-framework[bedrock]" # boto3>=1.34 (AWS Bedrock)
221
+
222
+ # All provider SDKs at once:
223
+ pip install "admina-framework[adapters]"
224
+
225
+ # Everything (proxy + NLP + telemetry + all adapters):
226
+ pip install "admina-framework[all]"
227
+ python -m spacy download en_core_web_sm # for [all] only
228
+
229
+ # Optional: Rust-accelerated engine (auto-detected at runtime).
230
+ # Opt-in extra — pulls in the admina-core wheel from PyPI.
231
+ pip install "admina-framework[rust]"
232
+
233
+ # The proxy for the OpenAI-compatible gateway only: without Redis,
234
+ # ClickHouse, boto3 and the scientific stack (see "Embedded deployment").
235
+ pip install "admina-framework[proxy-minimal]"
236
+
237
+ # Advanced: SDK only (no proxy, no dashboard, no `admina dev`).
238
+ # Use this when embedding the SDK into another service and you don't
239
+ # need the local dev server.
240
+ pip install admina-framework
241
+ ```
242
+
243
+ > The PyPI distribution name is `admina-framework`; the Python import
244
+ > name is `admina` (e.g. `from admina import GovernedModel`). This is
245
+ > a normal Python pattern — same as `python-dateutil` → `import dateutil`.
246
+
247
+ > The Rust engine is an **optional, opt-in** accelerator. The default
248
+ > `pip install admina-framework` ships only the pure-Python implementation;
249
+ > `admina-framework[rust]` adds the `admina-core` wheel, which Admina
250
+ > auto-detects at runtime: under `ADMINA_ENGINE=auto` (the default) the
251
+ > firewall and the loop breaker run on Rust whenever it is installed, as in
252
+ > the official proxy image (see [Engine selection](#engine-selection)).
253
+ >
254
+ > The default is pure Python on purpose: the Python injection firewall
255
+ > currently has **broader detection coverage** than the Rust one (it adds
256
+ > obfuscation-normalisation — homoglyph, leetspeak, base64, ROT13 — and a
257
+ > wider multilingual pattern set). Enable `[rust]` when per-request latency
258
+ > matters more than that extra coverage. See
259
+ > [Performance](#performance--hybrid-python--rust-engine) for the trade-off.
260
+
261
+ ### Or install from source
262
+
263
+ ```bash
264
+ git clone https://github.com/admina-org/admina.git
265
+ cd admina
266
+
267
+ # Recommended: proxy + dashboard + infra deps (enables `admina dev`)
268
+ pip install -e ".[proxy]"
269
+
270
+ # Everything (proxy + NLP + telemetry)
271
+ pip install -e ".[full]"
272
+
273
+ # All model-adapter SDKs
274
+ pip install -e ".[adapters]"
275
+
276
+ # Everything (proxy + NLP + telemetry + all adapters)
277
+ pip install -e ".[all]"
278
+
279
+ # CLI workflow
280
+ admina init my-project # Scaffold a governed AI project
281
+ cd my-project # admina dev runs from the project directory
282
+ admina dev # Start the local proxy + dashboard
283
+
284
+ # Full stack via Docker (no [proxy] extra required)
285
+ ./scripts/bootstrap-secrets.sh # Auto-generate .env with random credentials
286
+ docker compose up --build # Credentials printed at bootstrap
287
+
288
+ # Note: To use the OllamaAdapter, install Ollama (https://ollama.ai)
289
+ # and pull a model first: ollama pull llama3.1:8b
290
+
291
+ # Advanced: SDK only (no proxy, no dashboard)
292
+ pip install -e .
293
+ python -c "from admina import GovernedModel; print('SDK ready')"
294
+ ```
295
+
296
+ Dashboard: [http://localhost:3000](http://localhost:3000) | API docs: [http://localhost:8080/docs](http://localhost:8080/docs)
297
+
298
+ ## Architecture
299
+
300
+ Admina runs in **dual mode** — in-process via SDK or networked via proxy — but both modes feed the **same governance pipeline**.
301
+
302
+ ```mermaid
303
+ flowchart LR
304
+ A1["your code → GovernedModel.ask()"] --> P
305
+ A2["AI agent → POST /mcp"] --> P
306
+ P["governance pipeline"]
307
+ P --> U1["Ollama / OpenAI"]
308
+ P --> U2["MCP server / LLM"]
309
+ classDef pipe fill:#0ea5e9,stroke:#0369a1,color:#fff;
310
+ class P pipe;
311
+ ```
312
+
313
+ Pipeline (identical in both modes): `loop-breaker → firewall → PII redaction → guards → audit → forensic chain (SHA-256) → OTEL`
314
+
315
+ ## The 4 Governance Domains
316
+
317
+ | Domain | Capabilities | Engine |
318
+ |--------|-------------|--------|
319
+ | **Agent Security** | Anti-injection firewall (heuristic: 44 regex patterns on the Python engine, 15 on Rust, + scoring), loop breaker (TF-IDF cosine similarity) | Rust + Python |
320
+ | **Data Sovereignty** | PII redaction (email, SSN, credit cards, IBAN, phone, IP), residency enforcement, data classification | Rust + spaCy NER |
321
+ | **Compliance** | EU AI Act risk classification (Art. 6) and gap analysis (Art. 9-15), forensic black box (SHA-256 hash chain), OTEL native spans | Rust + Python |
322
+ | **AI Infrastructure** | LLM engine (Ollama, OpenAI), RAG pipeline (ChromaDB), Open WebUI | Python |
323
+
324
+ All governance domains operate **bidirectionally** — scanning both outbound requests and inbound responses.
325
+
326
+ ## SDK
327
+
328
+ Four governed primitives, each with async + sync interfaces:
329
+
330
+ ```python
331
+ from admina import GovernedModel, GovernedData, GovernedAgent, ComplianceKit
332
+ ```
333
+
334
+ | Primitive | Purpose | Governance applied |
335
+ |-----------|---------|-------------------|
336
+ | `GovernedModel` | LLM calls (Ollama, OpenAI) | PII redaction on prompts and responses, event audit |
337
+ | `GovernedData` | Data ingestion and queries | PII classification, residency enforcement, access audit |
338
+ | `GovernedAgent` | MCP/A2A agent calls | Firewall, PII, loop breaker — full proxy pipeline in-process |
339
+ | `ComplianceKit` | Regulatory compliance | EU AI Act risk classification, gap analysis, report generation |
340
+
341
+ ## Plugin System
342
+
343
+ 9 plugin interfaces, auto-discovered from `plugins/builtin/` or installed via CLI:
344
+
345
+ | Interface | Builtin implementations |
346
+ |-----------|------------------------|
347
+ | Model Adapter | Ollama, OpenAI, Anthropic, Gemini, Mistral, Bedrock, vLLM |
348
+ | Data Connector | ChromaDB, Filesystem |
349
+ | Governance Domain | GuardrailsAI (toxic, jailbreak, bias, PII) |
350
+ | Compliance Template | EU AI Act |
351
+ | Transport Adapter | MCP, HTTP REST |
352
+ | Forensic Store | Filesystem, S3-compatible (boto3 — AWS S3, MinIO, R2, …) |
353
+ | Auth Provider | API Key |
354
+ | PII Engine | spaCy + Regex (default), Microsoft Presidio (`pip install admina-framework[presidio]`, `ADMINA_PII_ENGINE=presidio`); engines of other packages through the `admina.pii_engines` entry-point group (see [PII engines](#pii-engines)) |
355
+ | Alert Channel | Log, Webhook |
356
+
357
+ Model adapters lazy-import their provider SDK, so install the ones you
358
+ use: `pip install admina-framework[adapters]` for all of them, or a
359
+ single provider (`[openai]`, `[ollama]`, `[anthropic]`, `[mistral]`,
360
+ `[gemini]`, `[bedrock]`). vLLM has no extra of its own — it serves an
361
+ OpenAI-compatible API and subclasses the OpenAI adapter, so install
362
+ `[openai]` for it.
363
+
364
+ ```bash
365
+ admina plugin list # List registered plugins
366
+ admina plugin install ./my-plugin # Install a custom plugin
367
+ admina plugin create my-domain # Scaffold a new plugin
368
+ ```
369
+
370
+ ## CLI
371
+
372
+ ```bash
373
+ admina init my-project # Scaffold project with admina.yaml + docker-compose.yml
374
+ admina dev # Local mode: proxy + dashboard on :3000 (no Docker)
375
+ admina dev --stack # Docker stack: + redis + clickhouse + grafana
376
+ admina dev --with-llm # --stack + ollama + chromadb + open-webui
377
+ admina plugin list # List all registered plugins
378
+ admina plugin install X # Install a plugin from path or registry
379
+ admina plugin create X # Scaffold a new plugin from template
380
+ admina forensic export --from-seq 1 --format jsonl --out records.jsonl
381
+ admina forensic verify # Verify the forensic chain (read-only, JSON result)
382
+ admina redteam --format md # Detection-efficacy scorecard of the firewall, PII and loop detectors
383
+ admina redteam --corpora-dir corpora/ --config admina.yaml --baseline baseline.json --gate
384
+ ```
385
+
386
+ `admina forensic export` and `admina forensic verify` read the directory of a
387
+ `filesystem` forensic store (`--dir`, default `$FORENSIC_BASE_DIR`) and never
388
+ write to it, so they can run while the proxy does. `export` writes the
389
+ records from `--from-seq` on, in sequence order, one per line: the bytes of
390
+ each record file as they are, then a newline (`--out FILE`, replaced once
391
+ complete, or `-` for standard output). `verify` prints the verification
392
+ result (`valid`, `records`, `reason`, `sequence_number`, `checkpoint`,
393
+ `last_hash`) and exits with 0 when the chain is valid, 1 when it is not;
394
+ `--checkpoint SEQ:HASH` (the `checkpoint` of an earlier result) checks only
395
+ the records after it.
396
+
397
+ `admina redteam` measures the injection firewall, the PII redactor and the
398
+ loop breaker on the corpora of the package (`admina/redteam/corpora/`), on
399
+ every available engine (`--engine both|python|rust`), then prints the
400
+ Markdown scorecard and writes the JSON one to `--out` (`--format
401
+ md|json|both`; `--corpus NAME` runs one corpus). `scripts/redteam.py` runs
402
+ the same command.
403
+
404
+ The packaged corpora are small (tens of samples per detector, a few per
405
+ language) and their labels are assigned by the maintainers, not by a third
406
+ party: read the scorecard as indicative, and measure on corpora of your own
407
+ traffic with `--corpora-dir`.
408
+
409
+ - `--corpora-dir DIR` adds external corpora, run after the packaged ones:
410
+ each `<name>.jsonl` has the rows of the packaged corpus of its detector
411
+ (`{"id", "text", "label": "attack" | "benign", "lang", "tag"}` for the
412
+ firewall, `expected_types` instead of `label` for the PII redactor,
413
+ `messages` instead of `text` with `label` `loop` | `not_loop` for the loop
414
+ breaker), and the directory's `SHA256SUMS` (the output of `sha256sum
415
+ *.jsonl`) lists every one; the hashes are verified before the run.
416
+ - `--config FILE` (default `$ADMINA_CONFIG`) builds the Python firewall from
417
+ the `agent_security.firewall` settings of an admina.yaml, as the proxy
418
+ does: custom patterns, pattern packs, disabled categories and patterns,
419
+ heuristic threshold and allowed tags. The Rust firewall is measured only
420
+ when the file sets none of the keys it cannot apply (see
421
+ [Engine selection](#engine-selection)); with `--engine rust`, such a file
422
+ is an error. The PII redactor and the loop breaker keep their defaults.
423
+ - `--write-baseline [FILE]` writes the baseline of the run (default:
424
+ `baseline.json` next to `--out`). `--baseline FILE` compares the run with
425
+ a baseline and prints the result on standard error; `--gate` exits with
426
+ status 1 when a recall drops or a false positive appears, when the number
427
+ of benign samples of a corpus differs from the baseline (`fp_samples`:
428
+ regenerate the baseline after changing a corpus), or when a corpus ran
429
+ only on engines the baseline does not declare (default baseline: the
430
+ packaged one, for the packaged corpora with the default settings).
431
+ - Exit status 2: an option, a corpus, the configuration or the baseline is
432
+ not valid (a hash that does not match, a row without `tag`, an unknown
433
+ corpus name), or `--engine rust` cannot run a selected corpus
434
+ (`admina-core` is not installed, or `--config` sets a key that only the
435
+ Python firewall applies).
436
+
437
+ `admina dev` defaults to a **single-process local mode** with zero Docker
438
+ dependency: one uvicorn serves the proxy API and the dashboard SPA on the
439
+ same port. Use `--stack` for the production-like Docker compose, or
440
+ `--with-llm` to also boot local LLM services.
441
+
442
+ ## Dashboard
443
+
444
+ Real-time governance dashboard on port 3000:
445
+
446
+ - **Governance Score** — 0-100 composite metric (data residency, audit coverage, attack rate, forensic integrity, EU AI Act compliance)
447
+ - **Live Feed** — streaming governance events via WebSocket
448
+ - **Compliance Gaps** — EU AI Act gap analysis with article-level detail
449
+ - **Infrastructure Health** — proxy, Redis, forensic store, ClickHouse, OTEL status
450
+
451
+ API backend: `GET /api/dashboard/score`, `/feed`, `/compliance`, `/sovereignty`, `/infra`, `/models`
452
+
453
+ In `admina dev` local mode the dashboard asks for the API key (`admina password show`)
454
+ and keeps a short-lived browser session that only the read-only dashboard API accepts.
455
+
456
+ In the Docker stack (`docker compose up`) the dashboard container is published on
457
+ `127.0.0.1:3000` only. With `ADMINA_API_KEY` set it asks for HTTP Basic Auth
458
+ (`ADMINA_DASHBOARD_USER`, default `admin`, and `ADMINA_DASHBOARD_PASSWORD`, which
459
+ `./scripts/bootstrap-secrets.sh` and `make up` generate) and adds the key only to the
460
+ dashboard's read-only routes (`/api/dashboard/*`, `/api/stats`); it does not start
461
+ without a password. `/mcp` and the other `/api/` routes are forwarded as received, so
462
+ callers send their own key. Without `ADMINA_API_KEY` the page signs in with the key
463
+ as in local mode.
464
+ Set `ADMINA_DASHBOARD_ENABLED=false` to stop serving the bundled dashboard.
465
+ The session cookie is `Secure` over HTTPS. `DASHBOARD_COOKIE_SECURE=auto` marks it
466
+ `Secure` on plain HTTP too, unless the dashboard is opened as `localhost` or a loopback
467
+ address: other hosts then sign in over HTTPS only. `DASHBOARD_COOKIE_SECURE=true` always
468
+ marks it `Secure`; `false` (the default) only over HTTPS.
469
+
470
+ ## Configuration
471
+
472
+ Admina uses `admina.yaml` as the primary config file (with `.env` fallback for backward compatibility):
473
+
474
+ ```bash
475
+ cp admina.yaml.example admina.yaml # Copy and customize
476
+ ```
477
+
478
+ See [`admina.yaml.example`](https://github.com/admina-org/admina/blob/main/admina.yaml.example) for all options including domains, AI infra, plugins, dashboard, forensic storage, alert channels, and integrations.
479
+
480
+ ### Configuration check
481
+
482
+ `admina.yaml` is checked against its schema (`schema_version: 1`,
483
+ `admina.core.config_schema`):
484
+
485
+ - A value of the wrong type (a word where a number goes, a single name where
486
+ a list goes, a list where a section goes) is an error naming the key:
487
+ `load_config()` raises `ConfigSchemaError` (a `ValueError`) and the proxy
488
+ does not start, for example `admina.yaml /etc/admina/admina.yaml:
489
+ domains.agent_security.loop_breaker.window_size: must be an integer`. The
490
+ engine factories of the SDK (`get_firewall()`, `get_pii_engine()`,
491
+ `pii_mask_style()`, `get_egress_policy()`) raise the same error, whatever
492
+ key it names, and `admina plugin list` exits with it. The
493
+ values of `gateway` and `presidio` are checked by their own readers, with
494
+ the same effect. An empty value (`key:` and nothing after it) is not
495
+ checked.
496
+ - A key the schema does not know, such as the typo
497
+ `domains.agent_security.firewal`, is logged at proxy startup as a warning
498
+ with its path (`admina.yaml /etc/admina/admina.yaml: unknown keys, not
499
+ read: domains.agent_security.firewal ...`). With `ADMINA_CONFIG_STRICT=true`
500
+ it is an error and the proxy does not start. `check_config()` of
501
+ `admina.core.config` runs the same check for the SDK.
502
+ - Free-form blocks are not checked inside: `plugin_config`, `integrations`,
503
+ `agent_security.domains`, and the entries of `custom_patterns` (the
504
+ firewall skips a malformed entry with a warning).
505
+
506
+ At startup the proxy also lists, as a warning, the `ADMINA_*` variables of
507
+ its environment and `.env` file that nothing reads (`ADMINA_* variables not read
508
+ by Admina: ADMINA_FOO ...`); `ADMINA_CONFIG_STRICT=true` makes them an
509
+ error. Known are the proxy settings, the variables read by the
510
+ engines, the SDK, the builtin plugins and the other containers of the stack,
511
+ and the per-route keys of the gateway. Variables of other components are not
512
+ reported when they start with:
513
+
514
+ - `ADMINA_<NAME>_`, where `<name>` is an entry point that an installed
515
+ distribution registers in `admina.plugins`, `admina.pii_engines` or
516
+ `admina.pattern_packs`, in upper case with `_` for any other character
517
+ than a letter or a digit (entry point `example-pii`:
518
+ `ADMINA_EXAMPLE_PII_`). A plugin reads its own variables under that
519
+ prefix;
520
+ - a prefix listed in `ADMINA_ENV_ALLOW_PREFIXES` (comma-separated, for
521
+ example `ADMINA_MYAPP_`).
522
+
523
+ Values are never logged.
524
+
525
+ ### Engine selection
526
+
527
+ `ADMINA_ENGINE` selects the engines of the firewall, the loop breaker and the
528
+ `spacy-regex` PII engine:
529
+
530
+ | `ADMINA_ENGINE` | Firewall | Loop breaker | PII (`spacy-regex`) |
531
+ |---|---|---|---|
532
+ | `auto` (default) | Rust when `admina-core` is installed, else Python; Python, with a warning, when `admina.yaml` sets a key below | Rust when `admina-core` is installed, else Python | Python |
533
+ | `python` | Python | Python | Python |
534
+ | `rust` | Rust | Rust | Rust |
535
+
536
+ With `ADMINA_ENGINE=rust` the engines are not built, and the proxy does not
537
+ start, when `admina-core` is not installed or when `admina.yaml` sets a key
538
+ that only the Python firewall applies: `agent_security.firewall`
539
+ `custom_patterns`, `disabled_categories`, `disabled_patterns` or
540
+ `pattern_packs` (an empty list is not set). The error names the cause:
541
+
542
+ ```
543
+ ADMINA_ENGINE=rust, but admina-core is not installed: install admina-framework[rust], or set ADMINA_ENGINE=python (or auto) to run the Python engines
544
+ ADMINA_ENGINE=rust, but admina.yaml sets agent_security.firewall.pattern_packs, which only the Python firewall applies: remove them, or set ADMINA_ENGINE=python (or auto) to run the Python firewall
545
+ ```
546
+
547
+ `pattern_pack_dirs`, `strict_pack_timing`, `heuristic_threshold` and
548
+ `allowed_tags` do not select an engine (the Rust engine does not read
549
+ them). The SDK raises the same `EngineSelectionError` (a `ValueError`) from
550
+ `get_firewall()`, `get_loop_breaker()` and `get_pii_engine()`.
551
+
552
+ Both firewalls are heuristic, and they differ: the Python firewall has 44
553
+ builtin patterns in 13 categories, normalises evasions (homoglyphs,
554
+ leetspeak, base64, ROT13, hyphenation) and has the Italian baseline; the
555
+ Rust firewall has 15 patterns, no normalisation and none of the Italian
556
+ baseline patterns, which are simply absent under Rust. See the
557
+ [model card](MODEL_CARD.md) for what each one detects.
558
+
559
+ `GET /health` and `GET /api/stats` report under `engine`:
560
+
561
+ - as in 0.12: `selection` (`ADMINA_ENGINE` as set, `auto` when unset),
562
+ `active` (the engine that selection resolves to for the firewall and the
563
+ loop breaker), `pii_active`, `rust_available`, `rust_version`
564
+ (`admina_core.version()`, or `null`) and `engine` (`rust` whenever
565
+ `admina-core` is installed);
566
+ - `firewall`, `loop_breaker` and `pii`: the engines of the objects the proxy
567
+ built. Under `auto` with a key above, `firewall` is `python` while
568
+ `active` is `rust`. `loop_breaker` is `null` when no enabled surface needs
569
+ one (`mcp`, `integration`); `pii` is `python`, `rust`, `presidio` or the
570
+ name of a plugin engine.
571
+
572
+ The startup banner and the `admina_engine_info` gauge of `/metrics` report
573
+ the same engines, `admina-core` version and settings:
574
+
575
+ ```
576
+ Engine selection: ADMINA_ENGINE=auto (admina-core 0.13.0)
577
+ Firewall: ON (rust engine) | PII Redaction: ON (python engine) | Loop Breaker: ON (rust engine)
578
+
579
+ admina_engine_info{engine="rust",firewall="rust",loop_breaker="rust",pii="python",pii_redaction="on",rust_available="yes",rust_version="0.13.0",selection="auto",version="0.13.0"} 1
580
+ ```
581
+
582
+ The gauge's `engine` is the firewall's engine, `loop_breaker` is `none`
583
+ when none is built, and `rust_version` is empty without `admina-core`.
584
+ `INJECTION_FAST_PATH_ENABLED=false` and `PII_REDACTION_ENABLED=false` show as
585
+ `OFF (gateway and /mcp)`: `/api/v1/validate` runs the firewall and the PII
586
+ redaction whatever these two settings say.
587
+
588
+ **The official proxy image** (`admina/proxy/Dockerfile`) installs the Rust
589
+ engine, and spaCy without a language model (the `slim` target has no spaCy).
590
+ Under the default `ADMINA_ENGINE=auto` it runs the Rust firewall and loop
591
+ breaker and the Python PII engine with regular expressions only: no names or
592
+ organisations are detected until a spaCy model is installed (see
593
+ [PII engines](#pii-engines)). Set `ADMINA_ENGINE=python` in the container
594
+ for the Python firewall, and read `engine.firewall` of `/health` to see the
595
+ firewall that runs.
596
+
597
+ ### Firewall patterns
598
+
599
+ Every firewall pattern has a stable id, reported with each match in
600
+ `patterns[].id` of the firewall check and of the forensic record:
601
+ `<category>.<language>.<n>` for the builtin categories written in English or
602
+ in several languages (`instruction_override.en.1`, `multilang_evasion.it.2`),
603
+ `<category>.<n>` for the `it_*` categories, `<pack>:<id>` for the patterns
604
+ of a [pattern pack](#firewall-pattern-packs) and `custom.<n>` for the
605
+ entries of `custom_patterns`, in their order. A builtin id never changes and
606
+ is never reused. `agent_security.firewall.disabled_patterns` leaves out single
607
+ patterns by id, where `disabled_categories` leaves out whole categories; an
608
+ unknown id is logged as a warning:
609
+
610
+ ```yaml
611
+ domains:
612
+ agent_security:
613
+ firewall:
614
+ disabled_patterns: [tool_abuse.en.4]
615
+ ```
616
+
617
+ The Python firewall has an Italian baseline besides the Italian patterns of
618
+ `multilang_evasion`: `it_instruction_override`, `it_role_hijack`,
619
+ `it_prompt_extraction` and `it_model_addressing` (risk `high`). Italian
620
+ imperatives of `-are` verbs have the form of the third person ("ignora",
621
+ "annulla"), so these patterns and the Italian `multilang_evasion` patterns
622
+ match such a verb where an instruction starts (the start of the text, after
623
+ a sentence end, a colon, a line break, an opening bracket, a table cell
624
+ `|`, an opening tag `<p>`, the start or the end of an HTML comment, an
625
+ opening quote or backtick; then closing tags such as `</b>`, a speaker
626
+ label such as `Utente>`, a list marker such as `-`, `1)`, `#` or `>`,
627
+ emphasis `**`, and up to two words such as "ok,", "grazie,", "per favore",
628
+ "assistente,"), after a
629
+ clause that starts there with a second-person imperative ("Traduci il
630
+ testo e ignora le istruzioni precedenti"; an opening quote, bracket or tag
631
+ inside the clause ends it), or with a second-person
632
+ object ("... e ignora le tue istruzioni"). Other patterns rest on
633
+ second-person forms ("rispondi", "sei ora", "mostrami") and on notes
634
+ addressed to an AI system ("Istruzioni per l'IA:"). "Ignora tutte le
635
+ istruzioni precedenti" matches; "Il giudice
636
+ annulla le linee guida impugnate" does not, and neither does an override
637
+ inside a sentence without one of these contexts ("Il documento è lungo,
638
+ ignora le istruzioni precedenti") or after a closing quote, tag or emphasis
639
+ that follows a word ('Il modulo "Alfa" ignora le istruzioni precedenti',
640
+ "<b>Il fornitore</b> ignora le istruzioni precedenti").
641
+
642
+ `custom_patterns`, `disabled_categories`, `disabled_patterns` and pattern
643
+ packs (below) apply to the Python firewall only, and so does the Italian
644
+ baseline: with any of those keys set, `ADMINA_ENGINE=auto` runs the Python
645
+ firewall and `ADMINA_ENGINE=rust` stops the proxy (see
646
+ [Engine selection](#engine-selection)).
647
+
648
+ #### Firewall pattern packs
649
+
650
+ A pattern pack is a named, versioned set of firewall patterns in a YAML or
651
+ JSON file, added to the builtin patterns when
652
+ `agent_security.firewall.pattern_packs` names it
653
+ ([`examples/pattern_packs/example-pack.yaml`](examples/pattern_packs/example-pack.yaml)):
654
+
655
+ ```yaml
656
+ name: example-pack # [a-z0-9][a-z0-9-]*, the file name without suffix
657
+ version: "1.0.0" # a string
658
+ description: Example patterns. # optional
659
+ patterns: # at least one
660
+ - id: internal_notes # [a-z0-9][a-z0-9_.-]*, unique in the pack
661
+ regex: "\\b(?:show|reveal|print)\\s++(?:me\\s++)?(?:the\\s++)?internal\\s++(?:ticket\\s++)?notes\\b"
662
+ category: example_disclosure # [a-z0-9][a-z0-9_]*
663
+ risk_level: high # low | medium | high | critical
664
+ ```
665
+
666
+ No other key is accepted. The firewall knows each pattern as
667
+ `<pack>:<id>` (`example-pack:internal_notes`): check results report it and
668
+ `disabled_patterns` accepts it. Pack patterns follow the builtin ones and
669
+ precede `custom_patterns`; `disabled_categories` applies to their
670
+ categories too.
671
+
672
+ ```yaml
673
+ domains:
674
+ agent_security:
675
+ firewall:
676
+ pattern_pack_dirs: [/etc/admina/packs] # or ADMINA_PATTERN_PACK_DIRS
677
+ pattern_packs: [example-pack]
678
+ strict_pack_timing: false
679
+ ```
680
+
681
+ Admina looks for each listed name among the entry points of the group
682
+ `admina.pattern_packs`, then in each directory of `pattern_pack_dirs`, in
683
+ order (`<name>.yaml`, `<name>.yml`, `<name>.json`; relative directories
684
+ from the working directory). `ADMINA_PATTERN_PACK_DIRS`, directories
685
+ separated by `:` (`;` on Windows), replaces `pattern_pack_dirs` when set.
686
+ An installed distribution provides a pack with an entry point whose loader
687
+ returns the pack mapping or the path of a pack file in its package data:
688
+
689
+ ```toml
690
+ [project.entry-points."admina.pattern_packs"]
691
+ example-pack = "example_pkg.packs:example_pack"
692
+ ```
693
+
694
+ A name must come from exactly one source. The firewall is not built, and
695
+ the proxy does not start, when a listed pack is not found (the error lists
696
+ the available packs), comes from two sources, is listed twice, or does not
697
+ validate (the error names the file or entry point and the key path, such as
698
+ `patterns[1].risk_level`), and when a pack directory is missing. Each pack
699
+ pattern is timed on 64k-character inputs when the firewall is built
700
+ ([pattern timing](#pattern-timing)): a pattern over 50 ms is logged as a
701
+ warning naming its id, or stops the firewall with
702
+ `strict_pack_timing: true`. Admina ships no pack of its own besides the
703
+ example.
704
+
705
+ #### Firewall deep path
706
+
707
+ The deep path scores five heuristic signals (imperative words, special
708
+ characters, context switches, length, escape sequences) and flags a text
709
+ whose score reaches `agent_security.firewall.heuristic_threshold` (default
710
+ `0.5`). Separators (`---`, `===`), `###`, code fences and tags count as
711
+ context switches, except the tags named in `allowed_tags` (any case), such
712
+ as the tag your application puts around retrieved documents. HTML entities
713
+ (`&euro;`, `&#8364;`) and percent-encoding (`%20`) are not escape sequences,
714
+ and only a text longer than 100 000 characters gets the length signal.
715
+ `INJECTION_DEEP_PATH_ENABLED=false` turns the deep path off: a text is then
716
+ flagged by the patterns only.
717
+
718
+ ```yaml
719
+ domains:
720
+ agent_security:
721
+ firewall:
722
+ heuristic_threshold: 0.5
723
+ allowed_tags: [source]
724
+ ```
725
+
726
+ `heuristic_threshold` and `allowed_tags` apply to the Python firewall; the
727
+ Rust engine scores with signals and a threshold of its own, and follows
728
+ `INJECTION_DEEP_PATH_ENABLED`.
729
+
730
+ #### Pattern timing
731
+
732
+ Custom firewall rules (`agent_security.firewall.custom_patterns`) run on
733
+ Python's backtracking `re` engine. Check each one on long inputs before
734
+ deploying it:
735
+
736
+ ```python
737
+ from admina.domains.agent_security.pattern_timing import measure_pattern, probe_pattern
738
+
739
+ probe_pattern(r"delete\s+user\s+\d+") # worst search time in ms on 64k-character inputs
740
+ measure_pattern(r"delete\s+user\s+\d+") # the same, with the slowest input and all timings
741
+ ```
742
+
743
+ A result above 50 ms flags a pattern that is too slow on some long input. One
744
+ whitespace quantifier between two literals, with the whitespace inside each
745
+ optional group and possessive quantifiers (`\s++`, `\s*+`) where a whitespace
746
+ run is followed by a literal, keeps the backtracking over a whitespace run
747
+ linear in the length of the run.
748
+
749
+ That rule does not limit how many positions a search starts from. A pattern
750
+ that begins with a repeated character class can start at every position of a
751
+ long run of that class and read to the end of the run each time:
752
+ `\b[\w.]+=\d` passes the probe, yet takes time proportional to the square of
753
+ the run length on `a.a.a.…` (no `=`). The generated inputs contain no such
754
+ runs, so time them as well:
755
+
756
+ ```python
757
+ import re
758
+ from admina.domains.agent_security.pattern_timing import search_ms
759
+
760
+ search_ms(re.compile(r"\b[\w.]+=\d"), "a." * 32768) # ms on a 64k-character run
761
+ ```
762
+
763
+ Anchoring the start of the run, `(?<![\w.])[\w.]++=\d`, gives one attempt per
764
+ run (a match then starts where the run starts).
765
+
766
+ ### Request size limits
767
+
768
+ `ADMINA_MAX_REQUEST_BYTES` (default 10 MiB, `0` = no limit) caps the request
769
+ body on every route. A body over the cap gets 413 before it is parsed: at once
770
+ when its `Content-Length` is over the cap, otherwise as soon as the bytes read
771
+ go over it. The 413 body is in the OpenAI error format on `/v1`
772
+ (`invalid_request_error`, code `request_too_large`) and `{"detail": ...}`
773
+ elsewhere. `MAX_REQUEST_TOKENS` (default 100000, `0` = no limit) caps the
774
+ request content on `/mcp`, estimated as the length in characters of the
775
+ scanned text. `ADMINA_GATEWAY_MAX_PROMPT_CHARS` (default `0`, no limit) caps
776
+ the message text of `POST /v1/chat/completions`, in characters (the text of
777
+ every message, as scanned); longer requests get 413 (`invalid_request_error`,
778
+ code `prompt_too_long`) before any governance check.
779
+
780
+ ### PII engines
781
+
782
+ `ADMINA_PII_ENGINE` (or `pii_engine` in `admina.yaml`, default `spacy-regex`)
783
+ selects the engine of PII redaction: a built-in engine (`spacy-regex`,
784
+ `presidio`) or an engine that another package registers in the
785
+ `admina.pii_engines` entry-point group. An unknown name stops the proxy at
786
+ startup with the list of the engines available.
787
+
788
+ ```toml
789
+ [project.entry-points."admina.pii_engines"]
790
+ example-pii = "example_pkg.engine:ExamplePIIEngine"
791
+ ```
792
+
793
+ The entry point names a `BasePIIEngine` subclass (`admina.plugins`), or a
794
+ callable that returns one; a `config` parameter receives the engine's block of
795
+ `plugin_config` in `admina.yaml`. Admina runs the engine through
796
+ `admina.engines.PIIEngineBridge`, which calls its asynchronous `detect` and
797
+ `redact` on an event loop of the engine's own, from any thread, and returns
798
+ `{"redacted_text", "entities", "categories", "count"}`; entities carry the
799
+ type, offsets and length of each span, never its text. `special_categories`
800
+ of the engine lists the special categories of personal data (GDPR art. 9 and
801
+ 10) among its types: `DataClassifier(special_categories=...)` classifies them
802
+ `restricted`, like the built-in `SPECIAL_CATEGORIES`.
803
+
804
+ `ADMINA_PII_MASK_STYLE` (or `pii_mask_style` in `admina.yaml`) chooses the
805
+ masks: `typed` (default) replaces each span with its type (`[EMAIL]`,
806
+ `[PERSON]`, …); `omissis` replaces each span with `[OMISSIS]`, in requests,
807
+ responses and streamed responses alike, so no type is left in the text. In
808
+ `omissis`, the categories an engine lists in `sentence_categories` (such as
809
+ health or judicial data) have their whole sentence replaced; the sentences
810
+ come from the engine's `sentences(text)`, by default a simple splitter (a
811
+ line break, or `.`, `!`, `?` followed by an upper-case word, ends a
812
+ sentence). A streamed response from such an engine is then released a whole
813
+ sentence at a time (at most 4096 characters held back).
814
+
815
+ `ADMINA_PRESIDIO_NLP_MODELS` (or `presidio.nlp_models` in `admina.yaml`) sets
816
+ the spaCy pipeline of each language of the `presidio` engine, for example
817
+ `it:blank,en:en_core_web_sm`: an installed model, or `blank`, a tokenizer with
818
+ no model and no NER (the pattern recognizers, such as e-mail, IBAN, fiscal
819
+ codes and phone numbers, still run). A configured model that is not installed
820
+ stops the proxy at startup: models are never downloaded. Without the setting,
821
+ each of `en_core_web_sm` and `it_core_news_sm` that is installed is used.
822
+
823
+ The PII engines work without network access: the `presidio` engine checks
824
+ e-mail domains against the public suffix list bundled with `tldextract`,
825
+ without downloading it or writing a cache. `ADMINA_OFFLINE=true` (default
826
+ `false`) also sets `HF_HUB_OFFLINE`, `TRANSFORMERS_OFFLINE` and
827
+ `HF_DATASETS_OFFLINE` to `1` before a PII engine is built and when the proxy
828
+ starts (engines built on Hugging Face libraries then load only local files),
829
+ and starts the proxy without the OpenTelemetry exporter.
830
+
831
+ Every engine masks text values only, never the keys of a JSON object: the
832
+ gateway redacts the text of each message (`content`, as a string or the
833
+ `text` of each part, reasoning and refusal text, tool call `arguments`) and
834
+ forwards roles, names and ids as received. A mask of Admina already in the
835
+ text (`[EMAIL]`, `[IBAN]`, …, `[OMISSIS]`) is never masked again; other text
836
+ in square brackets is masked like any other text. IBANs are masked when they
837
+ have the length of their country (Italy: 27 characters), compact or with
838
+ spaces, and a valid checksum; phone numbers include the Italian formats.
839
+
840
+ ### Embedded deployment
841
+
842
+ Settings for running the proxy as one component of a larger system (a
843
+ container, a service unit). Each one defaults to the behaviour described in
844
+ the rest of this README.
845
+
846
+ **Configuration file.** `ADMINA_CONFIG` names the `admina.yaml` to load, for
847
+ example `/etc/admina/admina.yaml`. When it is set, exactly that file is read
848
+ by the proxy, the engines and the SDK's `load_config()`; a missing,
849
+ unreadable or invalid file stops the proxy at startup instead of falling
850
+ back to the defaults. Unset (or empty), Admina looks for `admina.yaml` in the
851
+ current directory, then in the directory of the `admina` package.
852
+
853
+ **Secrets from files.** `ADMINA_API_KEY_FILE` and
854
+ `ADMINA_FORENSIC_STATE_KEY_FILE` name files holding the API key and the
855
+ forensic chain-state key (for example `/run/secrets/admina_api_key`), as the
856
+ upstream key files of the gateway do. Each file is read once at startup, with
857
+ one trailing newline removed. A missing, unreadable or empty file, or a key
858
+ set both directly and as a file, stops the proxy; the error names the
859
+ setting and the path, never the key. `ADMINA_FORENSIC_STATE_KEY_FILE` inside
860
+ the forensic directory stops it too: the key must be kept outside the store.
861
+
862
+ **Forensic store.** `FORENSIC_BACKEND` (`memory`, `filesystem`, `s3`) and
863
+ `FORENSIC_BASE_DIR` choose where the forensic records go. When they are not
864
+ set (in the environment or `.env`), the proxy reads
865
+ `domains.compliance.forensic.backend` (or its older name `storage`) and
866
+ `base_dir` from `admina.yaml`; without either, the backend is `memory`. A
867
+ value set in both places with different values is logged at startup, and
868
+ the environment's is used. The Docker Compose stack uses `filesystem` with
869
+ `FORENSIC_BASE_DIR=/app/.admina/forensic` on the named volume
870
+ `forensic-data`, which the proxy image creates owned by its user.
871
+
872
+ `ADMINA_FORENSIC_FAIL_MODE` says what happens when a record cannot be
873
+ written (a full disk, a directory that cannot be written, S3 errors):
874
+
875
+ | | `open` (default) | `closed` |
876
+ |---|---|---|
877
+ | Record not written | logged; the request is served without it | `503` and not forwarded: gateway `{"error": {..., "type": "server_error", "code": "forensic_unavailable"}}`, `/mcp` a JSON-RPC error (`-32603`), `/api/v1/audit` `503` |
878
+ | `/api/v1/validate` after a failed write | served | `503` until a record is written again |
879
+ | Backend that cannot be opened at startup (`filesystem` without a directory, or one that cannot be created or written; `s3` without boto3 or not reachable; a record that cannot be read) | an error is logged; nothing is recorded, not even in memory, and `/health` reports `forensic_writable: false` | the proxy does not start |
880
+
881
+ A record that is not written is never counted: the next one takes its
882
+ sequence number. Each record, `_chain_state.json` and its signature are
883
+ written atomically (a temporary file in the same directory, fsynced and
884
+ renamed, then the directory fsynced).
885
+
886
+ **Forensic records** (format `admina-forensic/1`). Each record is one JSON
887
+ object on one line, at `YYYY/MM/DD/HH/NNNNNNNN.json` under the store
888
+ directory (UTC hour of the write, sequence number on eight digits), with
889
+ `sequence_number` (from 1, contiguous), `timestamp_utc`,
890
+ `timestamp_unix_ms`, `previous_hash` (`GENESIS` for record 1, else the
891
+ `record_hash` of the record before), `event`, and:
892
+
893
+ - `record_hash`: SHA-256 (64 lowercase hex characters) of
894
+ `json.dumps(record, sort_keys=True, default=str)` (default separators,
895
+ UTF-8) of the record **without `record_hash`, `record_sig` and
896
+ `record_sig_alg`**;
897
+ - `record_sig`: HMAC-SHA256 (64 lowercase hex characters) of the 64 ASCII
898
+ characters of `record_hash`, under the record key = HMAC-SHA256 of
899
+ `admina-forensic/1 record signature` under the chain-state key
900
+ (`ADMINA_FORENSIC_STATE_KEY` or `ADMINA_FORENSIC_STATE_KEY_FILE`, kept
901
+ outside the forensic directory); `record_sig_alg`: `hmac-sha256`. Without
902
+ a key a record has `record_sig_alg: "none"` and no `record_sig`.
903
+
904
+ The chain state (`_chain_state.json`, with its HMAC-SHA256 in
905
+ `_chain_state.json.sig` when a key is set) holds `record_count`,
906
+ `chain_head`, `head_key` and `signed_from`: the first sequence number whose
907
+ record must be signed (records written before a key was set stay readable
908
+ and are reported as unsigned).
909
+
910
+ Verification (`GET /api/v1/forensic/verify`, `admina forensic verify`,
911
+ `verify_chain()`) reads one record at a time in sequence order and returns
912
+ `valid`, `records`, `last_hash`, `checkpoint` (`{"sequence_number",
913
+ "record_hash"}` of the last record checked: pass it back as
914
+ `?checkpoint=SEQ:HASH`, `--checkpoint SEQ:HASH` or `checkpoint=(seq, hash)`
915
+ to check only the records after it; `from_seq` starts from a sequence
916
+ number instead), `signed`, `unsigned`, `signatures_verified` (false without
917
+ the key) and, for the first failure, `sequence_number` and `reason`:
918
+
919
+ | `reason` | The record at `sequence_number` |
920
+ |---|---|
921
+ | `hash_mismatch` | is not a JSON object, or its `record_hash` is not the hash of its content |
922
+ | `sequence_gap` | has a `sequence_number` other than its file's, or comes twice or out of order |
923
+ | `signature_invalid` | has a `record_sig` that does not verify with the key, or an unknown `record_sig_alg` |
924
+ | `unsigned` | has no signature, though records from `signed_from` on must have one |
925
+ | `link_broken` | has a `previous_hash` that is not the `record_hash` of the record before it |
926
+ | `missing_record` | cannot be found: sequence numbers start at 1 and follow one another up to the chain state's count at least |
927
+ | `state_mismatch` | is the chain state's last record and has another hash |
928
+ | `checkpoint_mismatch` | is the checkpoint's and has another hash |
929
+ | `state_missing` | — there are records but no chain state |
930
+ | `state_invalid` | — the chain state cannot be read, or its HMAC does not verify with the key |
931
+ | `store_unavailable` | — the backend could not be opened |
932
+
933
+ **Chain state at startup.** The store reads the chain state and, with a
934
+ key, checks its HMAC. An S3 read is retried (`FORENSIC_S3_MAX_RETRIES`
935
+ times, backoff from `FORENSIC_S3_BASE_DELAY_S`); an object is missing only
936
+ when S3 answers that it does not exist (`NoSuchKey`), and any other error
937
+ still there after the retries is a read error, as for a file:
938
+
939
+ - a valid state is used; the record at its count must be its head, and a
940
+ record written after it (the process stopped between the record and the
941
+ state) is counted when it verifies and links to it;
942
+ - a missing state (with records), or one that cannot be read or does not
943
+ verify, is rebuilt **only** from records that all verify with the key,
944
+ from record 1 on (sequence, hashes, links, signatures). The rebuild is
945
+ logged at `CRITICAL` (`Forensic chain state rebuilt from verified
946
+ records`) and recorded as a signed record with `event.event_type:
947
+ "chain_state_rebuilt"`, `cause` (`state_missing` or `state_invalid`),
948
+ `records_verified` and `head_hash`. The chain state keeps the rebuild,
949
+ so `/health` reports `forensic_chain: "rebuilt"` after every restart
950
+ until an operator, with the proxy stopped, runs `admina forensic
951
+ acknowledge-rebuild` (with the key in `ADMINA_FORENSIC_STATE_KEY`): it
952
+ verifies the whole chain and, when it is valid, clears the status.
953
+ An external copy of the head (for example the `checkpoint` of the last
954
+ export) shows whether records after it are missing;
955
+ - otherwise (no key, a record that does not verify, a missing last record)
956
+ the chain is **invalid**: a `CRITICAL` log names the reason and the
957
+ record, `/health` reports `forensic_chain: "invalid"` and `status:
958
+ "degraded"`, no record is written, verification is never valid, and with
959
+ `ADMINA_FORENSIC_FAIL_MODE=closed` governed requests are answered `503`;
960
+ - a record that cannot be read keeps the backend from opening (see the
961
+ table above).
962
+
963
+ To recover from an invalid chain: run `admina forensic verify` (or `admina
964
+ doctor` for S3), with the key in the environment, to see the reason and the
965
+ record; stop the proxy; restore the forensic directory or bucket (records,
966
+ `_chain_state.json`, `_chain_state.json.sig`) from a backup, or move it
967
+ aside, keeping it, and start with an empty one; start the proxy. A chain
968
+ state that could not be read because the storage was unavailable (logged as
969
+ `Cannot read the forensic chain state`) needs only a restart once it can be
970
+ read again. A store written without a key, or before a key was set, cannot
971
+ be rebuilt: keep its chain state, or move it aside when a key is
972
+ introduced.
973
+
974
+ **Audit records.** `POST /api/v1/audit` records the event it receives with
975
+ `source: "api_v1_audit"` (a `source` in the request is kept as
976
+ `client_source`) and `submitted_by`: the credential the request was admitted
977
+ with (`api_key`, `append_key`, `user:<id>` or `unauthenticated`). An
978
+ `event_type` of the records the proxy writes itself (`mcp_request`,
979
+ `mcp_response`, `gateway_request`, `gateway_response`, `gateway_response_scan`,
980
+ `policy_violation`, `chain_state_rebuilt`) is refused with `400`.
981
+ `ADMINA_AUDIT_APPEND_KEY` (or `ADMINA_AUDIT_APPEND_KEY_FILE`) is a key that
982
+ this route accepts besides the API key and every other route refuses; unset
983
+ (the default), the route needs the API key.
984
+
985
+ **Surfaces.** `ADMINA_ENABLED_SURFACES` lists the surfaces the proxy serves,
986
+ comma-separated (empty = all of them):
987
+
988
+ | Surface | Routes |
989
+ |---------|--------|
990
+ | `gateway` | `/v1/*` (OpenAI-compatible gateway) |
991
+ | `mcp` | `/mcp`, `/mcp/*` |
992
+ | `integration` | `/api/v1/*` (validate, audit, forensic verify) |
993
+ | `compliance` | `/api/compliance/*` |
994
+ | `dashboard` | `/api/dashboard/*` (live feed and browser sign-in included), `/api/stats`, `/api/events`, the dashboard shell (`/`, `/heimdall.png`, `/vendor/*`) |
995
+
996
+ The routes of a disabled surface are not mounted and answer 404, with or
997
+ without the API key. `/health` and `/metrics` are always served. The loop
998
+ breaker is built only when `mcp` or `integration` is enabled, the
999
+ coordination detector and its quarantine refresh loop only with `mcp`, and
1000
+ the gateway's pipeline threads only with `gateway`.
1001
+
1002
+ **Egress per surface.** `agent_security.egress.surfaces` in `admina.yaml`
1003
+ lists the surfaces the egress stage runs on, among `gateway` (the text of
1004
+ the chat messages of `POST /v1/chat/completions`), `mcp` (the arguments of
1005
+ `/mcp` tool calls), `integration` (`/api/v1/validate`) and `sdk`
1006
+ (`GovernedModel.ask()` and `stream()`). Unset, the stage runs on every one;
1007
+ an empty list runs it on none. An unknown name, or a value that is not a
1008
+ list, stops the proxy at startup (the SDK raises `ValueError`). To check
1009
+ tool calls only:
1010
+
1011
+ ```yaml
1012
+ domains:
1013
+ agent_security:
1014
+ egress:
1015
+ enabled: true
1016
+ surfaces: [mcp]
1017
+ allow: [api.example.com]
1018
+ ```
1019
+
1020
+ With `gateway` left out, the gateway does not evaluate the text of chat
1021
+ messages for destinations (a message that starts with a URL off the
1022
+ allowlist is not refused by the stage) and its records have no
1023
+ `checks.egress`.
1024
+
1025
+ **Optional dependencies.** `redis` is imported only when `REDIS_URL` has a
1026
+ Redis scheme, `clickhouse_connect` only when `CLICKHOUSE_HOST` is not empty
1027
+ and `boto3` only when `FORENSIC_BACKEND=s3`. `REDIS_URL=` and
1028
+ `CLICKHOUSE_HOST=` (empty) turn Redis and ClickHouse off with no connection
1029
+ attempt. The `proxy-minimal` extra installs the proxy without Redis,
1030
+ ClickHouse, boto3, typer and the scientific stack of the Python loop breaker:
1031
+
1032
+ ```bash
1033
+ pip install "admina-framework[proxy-minimal]"
1034
+ ADMINA_ENABLED_SURFACES=gateway REDIS_URL= CLICKHOUSE_HOST= \
1035
+ ADMINA_API_KEY_FILE=/run/secrets/admina_api_key \
1036
+ uvicorn admina.proxy.main:app --host 0.0.0.0 --port 8080
1037
+ ```
1038
+
1039
+ With `proxy-minimal`, the `mcp` and `integration` surfaces need the `proxy`
1040
+ extra (or `rust`, whose loop breaker needs no scientific stack); the proxy
1041
+ does not start when they are enabled without it.
1042
+
1043
+ **Health.** `GET /health` (public) reports:
1044
+
1045
+ ```json
1046
+ {
1047
+ "status": "healthy",
1048
+ "service": "admina-proxy",
1049
+ "version": "0.13.0",
1050
+ "mode": "enforce",
1051
+ "surfaces": ["gateway"],
1052
+ "ruleset_sha256": "9cada1f2c9c85e60d1ec53ede74fbec7524f1db6e324500c46105f4d174c980a",
1053
+ "forensic_writable": true,
1054
+ "forensic_chain": "ok",
1055
+ "engine": {
1056
+ "engine": "rust",
1057
+ "rust_available": true,
1058
+ "rust_version": "0.13.0",
1059
+ "selection": "auto",
1060
+ "active": "rust",
1061
+ "pii_active": "python",
1062
+ "firewall": "rust",
1063
+ "loop_breaker": null,
1064
+ "pii": "python"
1065
+ },
1066
+ "timestamp": "2026-09-28T23:37:06.472687+00:00"
1067
+ }
1068
+ ```
1069
+
1070
+ - `status`: `healthy`, or `degraded` while forensic records cannot be
1071
+ written (`forensic_writable` is `false`, the last record or chain-state
1072
+ write failed, or the chain is invalid); the HTTP status is 200 either way;
1073
+ - `forensic_chain`: `ok`, `rebuilt` (the chain state was rebuilt from
1074
+ verified records, and the rebuild has not been acknowledged) or `invalid` (see
1075
+ [Embedded deployment](#embedded-deployment), *Chain state at startup*);
1076
+ `null` for the `memory` store;
1077
+ - `mode`: the governance mode (`enforce`, `observe` or `dry-run`);
1078
+ - `surfaces`: the enabled surfaces;
1079
+ - `ruleset_sha256`: the active firewall ruleset, the value of
1080
+ `X-Admina-Ruleset` (see [Firewall ruleset](#firewall-ruleset));
1081
+ - `forensic_writable`: whether the forensic store accepts writes. With the
1082
+ `filesystem` backend a probe file is created, written, fsynced and removed
1083
+ in `FORENSIC_BASE_DIR`; with `s3` it is the result of the last record
1084
+ write (`null` before the first); with `memory` it is `null`; for a
1085
+ backend that could not be opened at startup it is `false`. The check
1086
+ runs at most once every 10 s, on a thread of its own, and its result is
1087
+ reused until then; a check that takes longer than 1 s reports `false`, so
1088
+ `/health` answers within about a second even when the store stalls.
1089
+
1090
+ **Logs.** `ADMINA_LOG_FORMAT=json` writes one JSON object per line
1091
+ (`timestamp`, `level`, `logger`, `message` and `exception` when there is
1092
+ one), uvicorn's own lines included; `text` is the default. An exception
1093
+ raised while a request or a response is governed (by a governance guard,
1094
+ the PII engine, the pipeline or the upstream exchange of the gateway,
1095
+ `/mcp` and `/api/v1/validate`) is logged by its class name, and at `DEBUG`
1096
+ with the frames of its traceback (`admina.core.exception_log`), never with
1097
+ its message, which can quote the governed text; the `error` of a guard's
1098
+ `ERROR` check (`checks["guard_<name>"]` in the forensic records and the
1099
+ ClickHouse `details`) is the class name too. An `/mcp` request whose
1100
+ governance pipeline raises is answered `500` (JSON-RPC `-32603`,
1101
+ `Internal proxy error`), a `POST /api/v1/validate` request `500`
1102
+ (`{"detail": "Internal Server Error"}`).
1103
+
1104
+ **`/metrics` and the API docs.** Both are public by default.
1105
+ `ADMINA_METRICS_REQUIRE_AUTH=true` and `ADMINA_API_DOCS_REQUIRE_AUTH=true`
1106
+ put `/metrics` and `/docs`, `/redoc`, `/openapi.json` behind the API key
1107
+ (`X-API-Key` or `Authorization: Bearer`); a browser opening `/docs` then
1108
+ cannot load the schema. `ADMINA_API_DOCS_ENABLED=false` removes the docs.
1109
+
1110
+ **Governed requests.** Each request the proxy governs on the gateway
1111
+ (`POST /v1/chat/completions`), on `/mcp` and on `POST /api/v1/validate`
1112
+ (the `integration` surface) is counted on `/metrics` and emits one
1113
+ `governance.decision` event on the event bus, which the dashboard live feed,
1114
+ the OpenTelemetry exporter and the alert channels read (one alert per
1115
+ `BLOCK` or `CIRCUIT_BREAK`). With ClickHouse configured it is also a row of
1116
+ `governance_events`: `event_type` `gateway_request`, `mcp_request` or
1117
+ `validate_request`, `request_hash` the event's `request_sha256`, and for
1118
+ the gateway `response_hash` the SHA-256 of the response sent. A gateway
1119
+ request is recorded once its response has ended, an `/mcp` request once it
1120
+ has been answered: each with the action of its response.
1121
+
1122
+ - `admina_requests_total{surface,action}`: counter per surface (`gateway`,
1123
+ `mcp`, `integration`; the enabled ones have samples from startup) and
1124
+ action: `ALLOW`, `BLOCK`, `REDACT` (allowed, with PII masked in the
1125
+ request), `CIRCUIT_BREAK`, or `ERROR` (the request failed in the proxy
1126
+ before the governance pipeline decided). A gateway completion answered
1127
+ with the block message after the upstream answered (flagged by the
1128
+ response scan, or whose PII redaction did not finish) is a `BLOCK`; so
1129
+ is an `/mcp` request whose response a governance guard blocks (its
1130
+ `inspect_response` verdict, or its contract error with
1131
+ `ADMINA_GUARD_FAIL_MODE=closed`), with the guard's `risk_level` (`HIGH`
1132
+ for a contract error), and so are `/mcp` requests over the rate limits or
1133
+ `MAX_REQUEST_TOKENS`.
1134
+ Requests refused before they are governed are not counted: gateway
1135
+ requests answered before they have an event id (unknown route, a body
1136
+ that is not a JSON object, a model outside the allowlist, a refused
1137
+ value, a prompt over `ADMINA_GATEWAY_MAX_PROMPT_CHARS`), and
1138
+ `/api/v1/validate` requests answered `400` or `503`.
1139
+ - `admina_request_duration_seconds{surface}`: histogram of the time from
1140
+ the arrival of a request to the end of its response, the upstream's time
1141
+ included (buckets from 5 ms to 300 s).
1142
+ - `admina_governance_duration_seconds{surface}`: histogram of the time the
1143
+ governance pipeline took to decide (buckets from 0.5 ms to 5 s).
1144
+ - `admina_requests_blocked_total` (`BLOCK`, `CIRCUIT_BREAK`),
1145
+ `admina_requests_allowed_total` (`ALLOW`, `REDACT`),
1146
+ `admina_requests_redacted_total` (`REDACT`) and `admina_avg_latency_ms`
1147
+ (the mean request duration) count every governed surface, as do the
1148
+ `requests_*` counters of `/api/stats`.
1149
+
1150
+ Label values come from these fixed sets only, never from a request. The
1151
+ metadata of a `governance.decision` event is `surface`, `event_id` (of the
1152
+ request's forensic records; a new id for `/api/v1/validate`), `domain` (the
1153
+ part of the pipeline that decided: `firewall`, `pii`, `loop_breaker`, a
1154
+ guard's name, `pipeline`, `response_firewall`, `response_pii`,
1155
+ `response_guard` or `none`),
1156
+ `latency_us` (the pipeline's time), `categories` (firewall category names),
1157
+ `pii_count`, `request_sha256` and, in `observe` and `dry-run` mode,
1158
+ `would_action`: names, counts and hashes, never text of a request or of a
1159
+ response. `request_sha256` is, for the gateway, the `request_sha256` of the
1160
+ request record; for `/mcp`, the SHA-256 of the JSON-RPC request as the
1161
+ proxy serialises it; for `/api/v1/validate`, the SHA-256 of `content`.
1162
+
1163
+ **OpenTelemetry.** With `OTEL_ENABLED=true` (the default), the `telemetry`
1164
+ extra installed and `ADMINA_OFFLINE` off, the proxy exports to
1165
+ `OTEL_ENDPOINT` (OTLP gRPC, default `http://localhost:4317`; an empty value
1166
+ leaves the endpoint to the OpenTelemetry SDK: `OTEL_EXPORTER_OTLP_ENDPOINT`,
1167
+ else `http://localhost:4317`) a span per `governance.decision` event, with
1168
+ `admina.domain`, `admina.action`, `admina.risk_level`, `admina.latency_us`,
1169
+ `admina.session_id` and `admina.meta.<key>` for each key of the event's
1170
+ metadata, and the span of each gateway chat completion.
1171
+ `OTEL_ENABLED=false` builds no exporter: nothing is exported and no
1172
+ connection is made for telemetry.
1173
+
1174
+ **Container entrypoint.** `admina/proxy/docker-entrypoint.sh` accepts
1175
+ `ADMINA_API_KEY` or `ADMINA_API_KEY_FILE` and prints only whether the key
1176
+ is set, never any part of it.
1177
+
1178
+ ### OpenAI-compatible gateway
1179
+
1180
+ The proxy serves an OpenAI-compatible API at `/v1` (`POST /v1/chat/completions`,
1181
+ streaming and non-streaming, and `GET /v1/models`). It runs the governance
1182
+ pipeline on each chat completion and forwards requests to an upstream route.
1183
+ By default there is one route, `default`, to `ADMINA_GATEWAY_UPSTREAM`
1184
+ (`http://localhost:11434/v1`), and no credentials are sent upstream.
1185
+
1186
+ `ADMINA_GATEWAY_MODELS_ALLOWLIST` (comma-separated model ids; empty, the
1187
+ default, = every model) limits the models: `GET /v1/models` lists only those,
1188
+ and a chat completion for any other model, or without a model, gets 403 in
1189
+ the OpenAI error format (`invalid_request_error`, `param: "model"`, code
1190
+ `model_not_allowed`) before any governance check, forensic record or upstream
1191
+ call.
1192
+
1193
+ A chat completion is forwarded with its body as received and its messages as
1194
+ governed (PII redacted when redaction applies). Three settings, all off by
1195
+ default, change the other top-level fields of the forwarded body:
1196
+
1197
+ | Setting | Default | Meaning |
1198
+ |---|---|---|
1199
+ | `ADMINA_GATEWAY_FORWARD_FIELDS` | empty: every field | fields forwarded, comma-separated and case-sensitive; `model`, `messages` and `stream` always are, and so are the fields of a limit below that is set; any other field is left out |
1200
+ | `ADMINA_GATEWAY_MAX_N` | `0`: no limit | largest `n` forwarded: a larger `n` is lowered to it; an absent or `null` `n` is forwarded as it is |
1201
+ | `ADMINA_GATEWAY_MAX_COMPLETION_TOKENS` | `0`: no limit | largest `max_tokens` and `max_completion_tokens` forwarded: each one that is larger is lowered to it, and a request that sets neither (absent or `null`) is forwarded with `max_tokens` set to it |
1202
+
1203
+ ```bash
1204
+ ADMINA_GATEWAY_FORWARD_FIELDS=temperature,top_p,stop,seed,tools,tool_choice,response_format,stream_options
1205
+ ADMINA_GATEWAY_MAX_N=1
1206
+ ADMINA_GATEWAY_MAX_COMPLETION_TOKENS=4096
1207
+ ```
1208
+
1209
+ While a limit is set, the fields it applies to must be absent, `null` or an
1210
+ integer of at least 1 (`true`, `2.0` and `"2"` are not); any other value gets
1211
+ 400 in the OpenAI error format (`invalid_request_error`, `param` naming the
1212
+ field, code `invalid_value`) before any governance check, forensic record or
1213
+ upstream call. The proxy does not start when `ADMINA_GATEWAY_FORWARD_FIELDS`
1214
+ names a field with characters other than ASCII letters, digits, `_` and `-`.
1215
+ These settings change the forwarded body only: the firewall scans the request
1216
+ as received, and the forwarded `messages`, whose hash is `request_sha256`, are
1217
+ the same with or without them.
1218
+
1219
+ Named routes come from `ADMINA_GATEWAY_UPSTREAMS` or from `gateway.upstreams`
1220
+ in `admina.yaml`; the environment variable, when set, replaces the YAML routes
1221
+ (URLs and key files):
1222
+
1223
+ ```bash
1224
+ ADMINA_GATEWAY_UPSTREAMS=main=http://main.upstream.test/v1,util=http://util.upstream.test/v1
1225
+ ADMINA_GATEWAY_UPSTREAM_API_KEY_FILE=/run/secrets/upstream_api_key
1226
+ ```
1227
+
1228
+ ```yaml
1229
+ gateway:
1230
+ upstreams:
1231
+ main: { url: "http://main.upstream.test/v1", api_key_file: /run/secrets/upstream_api_key }
1232
+ util: { url: "http://util.upstream.test/v1", api_key_file: /run/secrets/upstream_api_key }
1233
+ default_upstream: main
1234
+ ```
1235
+
1236
+ A request picks a route with the `X-Admina-Upstream` header. Without it the
1237
+ gateway uses `default_upstream`, or else the first route; an unknown route name
1238
+ gets a 400 response in the OpenAI error format (`invalid_request_error`, code
1239
+ `unknown_upstream`) and is neither scanned nor recorded. The route name is
1240
+ stored in the forensic record (`upstream`).
1241
+
1242
+ The upstream receives `Authorization: Bearer <key>` when the route has a key.
1243
+ The key of a route is the first one set, in this order:
1244
+
1245
+ | Source | Scope |
1246
+ |---|---|
1247
+ | `ADMINA_GATEWAY_UPSTREAM_<NAME>_API_KEY` or `…_API_KEY_FILE` (`<NAME>`: route name in upper case) | one route |
1248
+ | `api_key_file` of the route in `admina.yaml` | one route |
1249
+ | `ADMINA_GATEWAY_UPSTREAM_API_KEY` or `ADMINA_GATEWAY_UPSTREAM_API_KEY_FILE` | every route |
1250
+
1251
+ Key files are read once at startup and one trailing newline is removed. The
1252
+ proxy does not start when a key file is missing, unreadable or empty, when a
1253
+ key is set both directly and as a file, when a route is malformed or when
1254
+ `default_upstream` names no route. Keys are masked in the settings
1255
+ representation and are not logged. The caller's `Authorization`, `X-API-Key`,
1256
+ `Cookie` and `X-Admina-*` headers are never forwarded upstream; other headers
1257
+ only when listed in `ADMINA_GATEWAY_FORWARD_HEADERS` (see
1258
+ [Correlation and forensic records](#correlation-and-forensic-records)).
1259
+
1260
+ #### Upstream responses, errors and timeouts
1261
+
1262
+ `ADMINA_GATEWAY_STREAM_MODE`, or `gateway.stream_mode` in `admina.yaml` (the
1263
+ environment variable wins), sets how streamed chat completions are relayed:
1264
+
1265
+ | Mode | Behaviour |
1266
+ |---|---|
1267
+ | `passthrough` (default) | While no response transformation is active (`PII_REDACTION_ENABLED=false`), the client receives the upstream bytes unchanged, every field included, each SSE event as soon as it is complete. With PII redaction on, the gateway relays as in `governed`. |
1268
+ | `governed` | Each SSE chunk is parsed and re-serialised, one chunk for each upstream chunk, with all of its fields. With PII redaction on, every string of a choice is redacted, per choice and per field across chunks: `content` (a string or a list of parts), reasoning text, tool and function call `arguments` and any other field; text held back to catch an entity split across chunks is sent with the choice's finish chunk, at the same place, or in a last chunk at the end of the stream. `data: [DONE]` is sent when the upstream sends it. |
1269
+
1270
+ A non-streaming response is forwarded unchanged unless PII redaction is on;
1271
+ then the gateway parses it, and a successful response that is not a JSON
1272
+ object gets 502 (code `upstream_invalid_response`). An upstream error (4xx,
1273
+ 5xx) reaches the client with its status, body and content type, streaming or
1274
+ not.
1275
+
1276
+ With PII redaction on, in streamed and non-streaming completions alike:
1277
+
1278
+ - structural values are kept as they are: `index`, `id`, `type`, `role`,
1279
+ `name` and `finish_reason`, and the completion's `id`, `object`,
1280
+ `created`, `model`, `system_fingerprint` and `service_tier`;
1281
+ - `logprobs` and `token_ids` of each choice are sent as `null`, since
1282
+ generated text split into tokens cannot be redacted token by token;
1283
+ - other strings outside the choices, and SSE comment lines, are redacted as
1284
+ whole values;
1285
+ - values nested more than 16 levels deep are dropped.
1286
+
1287
+ The gateway talks to its upstreams through a client of its own:
1288
+
1289
+ | Setting | Default | Meaning |
1290
+ |---|---|---|
1291
+ | `ADMINA_GATEWAY_TIMEOUT_CONNECT` | `30` | seconds to open a connection or get a free one from the pool |
1292
+ | `ADMINA_GATEWAY_TIMEOUT_READ` | `30` | seconds to wait for the next upstream bytes (and for each write of the request) |
1293
+ | `ADMINA_GATEWAY_TIMEOUT_TOTAL` | `0` | seconds for the whole upstream exchange, from the request to the last byte |
1294
+ | `ADMINA_GATEWAY_MAX_CONNECTIONS` | `100` | size of the connection pool |
1295
+ | `ADMINA_GATEWAY_MAX_KEEPALIVE_CONNECTIONS` | `20` | idle connections kept open |
1296
+
1297
+ A timeout of `0` means no limit. A timeout before the response starts gets
1298
+ 504, any other connection failure 502, both with an OpenAI-style body
1299
+ (`{"error": {"message", "type": "upstream_error", "param", "code"}}`, code
1300
+ `upstream_timeout` or `upstream_error`). A failure during a stream ends it
1301
+ with one `data: {"error": {...}}` event and no `data: [DONE]`. When the client
1302
+ disconnects, the gateway closes the upstream request.
1303
+
1304
+ #### Governance pipeline threads and time budget
1305
+
1306
+ The gateway runs the governance pipeline (firewall, PII redaction, egress
1307
+ analysis, governance guards) and the PII redaction of completions in a pool of
1308
+ worker threads, so that scanning one request does not hold up the others:
1309
+
1310
+ | Setting | Default | Meaning |
1311
+ |---|---|---|
1312
+ | `ADMINA_GATEWAY_PIPELINE_WORKERS` | `0` | threads in the pool, the most requests governed at once (`0` = the number of CPUs); further requests wait for a free thread |
1313
+ | `ADMINA_GATEWAY_PIPELINE_TIMEOUT` | `0` | seconds a request waits for its governance decision, the wait for a free thread included (`0` = no limit) |
1314
+
1315
+ A request whose decision takes longer than `ADMINA_GATEWAY_PIPELINE_TIMEOUT` is
1316
+ blocked, in every governance mode, and never forwarded; its forensic record has
1317
+ `checks.pipeline = {"action": "BLOCK", "reason": "time_budget_exceeded",
1318
+ "budget_ms": <budget>}`. The thread that was scanning it stays busy until the
1319
+ scan ends. A request whose pipeline raises (in the firewall, PII redaction,
1320
+ egress analysis or a guard) is blocked the same way, in every governance mode,
1321
+ since its checks may not have run; the record has `checks.pipeline =
1322
+ {"action": "ERROR", "error": "<exception class>"}`. A guard contract error
1323
+ (`ValueError`, `RuntimeError`, `OSError` or `TypeError` from a guard) is
1324
+ handled inside the pipeline and follows `ADMINA_GUARD_FAIL_MODE`, as on the
1325
+ other surfaces; its check is `{"action": "ERROR", "error": "<exception
1326
+ class>"}`.
1327
+
1328
+ With PII redaction on, the redaction of each completion, and of each line of
1329
+ a stream, runs in the worker threads within the same time budget. A
1330
+ non-streaming completion whose redaction runs over the budget or raises is
1331
+ replaced by the block message (`finish_reason: "content_filter"`); a stream
1332
+ whose redaction runs over the budget or raises ends with one
1333
+ `data: {"error": {...}}` event (code `response_redaction_failed`) and no
1334
+ `data: [DONE]`. The text of that completion or line is not sent.
1335
+ Governance guards run in the worker threads too, each thread with an event loop
1336
+ of its own, so one guard instance can be called by several threads at once,
1337
+ each call on a different event loop. A guard must be thread-safe and must not
1338
+ keep objects bound to one event loop (an `asyncio.Lock`, an `httpx.AsyncClient`
1339
+ with pooled connections) across calls; see `BaseGovernanceGuard`. The built-in
1340
+ GuardrailsAI guard runs one validation at a time.
1341
+
1342
+ With `ADMINA_GATEWAY_SCAN_RESPONSE=true` (default `false`) the firewall also
1343
+ checks the completion text, the `content` of each choice, in the same worker
1344
+ threads and time budget (and only while `INJECTION_FAST_PATH_ENABLED` is on):
1345
+
1346
+ - a non-streaming completion is checked before it is returned; when it is
1347
+ flagged in `enforce` mode, or its check runs over the time budget, the client
1348
+ receives the block message instead (`finish_reason: "content_filter"`);
1349
+ - a streamed completion is checked after the last event has been sent: the
1350
+ text has already reached the client, so the outcome is only recorded.
1351
+
1352
+ Each check writes a forensic record of its own, `event_type:
1353
+ "gateway_response_scan"`, with `request_event_id` (the `event_id` of the
1354
+ request record), `stream`, `action` (`BLOCK` or `ALLOW`), `risk_level`,
1355
+ `would_action: "BLOCK"` when flagged but not blocked (a stream, or `observe`
1356
+ mode) and `checks.response_firewall` (names and signals, no text). Upstream
1357
+ errors and responses that are not a JSON object are not checked.
1358
+
1359
+ `/metrics` serves `admina_event_loop_lag_seconds`, a histogram of how late the
1360
+ proxy's event loop wakes up a task that sleeps 0.1 s at a time (buckets from
1361
+ 1 ms to 5 s, with `_sum` and `_count`): the time the loop spent on other work
1362
+ before it could run it. It stays around a millisecond while nothing holds up
1363
+ the loop.
1364
+
1365
+ #### Firewall ruleset
1366
+
1367
+ `ruleset_sha256()` (`admina.domains.agent_security.ruleset`) names the firewall
1368
+ rules a configuration applies: the SHA-256, as 64 lowercase hex characters, of
1369
+ the RFC 8785 (JCS) serialisation of `ruleset_format` (1, the version of this
1370
+ form), the Admina version, the engine, the active
1371
+ builtin patterns (Python engine) or the `admina-core` version (Rust engine),
1372
+ `pattern_packs`, `custom_patterns`, `disabled_categories`,
1373
+ `disabled_patterns`, `allowed_tags` and `heuristic_threshold` in
1374
+ thousandths. The exact form is in the module
1375
+ docstring; `ruleset_document()` returns that serialisation as text, to
1376
+ compare two rulesets. The SDK can compute it from `admina.yaml` without the
1377
+ proxy:
1378
+
1379
+ ```python
1380
+ from admina.core.config import load_config
1381
+ from admina.domains.agent_security.ruleset import ruleset_document, ruleset_sha256
1382
+ from admina.sdk import active_ruleset_sha256
1383
+
1384
+ ruleset_sha256(load_config("admina.yaml")) # Python engine
1385
+ ruleset_sha256(load_config("admina.yaml"), engine="rust") # Rust engine
1386
+ ruleset_document(load_config("admina.yaml")) # the hashed text
1387
+ active_ruleset_sha256() # the engine get_firewall() selects, as the proxy does
1388
+ ```
1389
+
1390
+ The proxy computes it at startup for the engine its firewall runs on. Every
1391
+ `POST /v1/chat/completions` response carries it in `X-Admina-Ruleset`: allowed,
1392
+ blocked and error responses, the 401 of authentication and the 413 of the
1393
+ request size limit included. An unexpected failure before the response starts
1394
+ gets a 500 with the header and an OpenAI-style body (`type: "server_error"`,
1395
+ code `internal_error`). `GET /v1/admina/ruleset` (API key required) returns:
1396
+
1397
+ ```json
1398
+ {
1399
+ "ruleset_sha256": "<64 hex>",
1400
+ "ruleset_format": 1,
1401
+ "ruleset_document": "{\"admina_version\":\"<version>\",...}",
1402
+ "engine": "python",
1403
+ "admina_core_version": null,
1404
+ "admina_version": "<version>",
1405
+ "accepted_prescan_rulesets": ["<64 hex>"],
1406
+ "prescan_tags": [],
1407
+ "scan_roles": ["system", "user", "assistant", "tool"],
1408
+ "scan_policy_enabled": false
1409
+ }
1410
+ ```
1411
+
1412
+ #### Scanned text
1413
+
1414
+ The firewall of the gateway scans every string of a chat completion request,
1415
+ keys included: the messages (content, names, tool calls), the tool
1416
+ definitions (`tools`: names, descriptions, parameter schemas),
1417
+ `response_format` and any other field of the body. The `arguments` of a tool
1418
+ call (`tool_calls[].function.arguments`, and a legacy
1419
+ `function_call.arguments`) are scanned as the JSON they hold, each string
1420
+ separately; arguments that are not JSON are scanned as they are. The scan
1421
+ scope below narrows the messages only: the tool definitions and the other
1422
+ fields are always scanned.
1423
+
1424
+ The scan follows the body 32 levels deep: the body is level 0, its fields
1425
+ level 1, and the JSON of tool call arguments is at the level of its string.
1426
+ A request with a string nested deeper is blocked in `enforce` mode
1427
+ (`X-Admina-Would-Action: BLOCK` in `observe` and `dry-run`), and its record
1428
+ has `checks.scan_depth = {"action": "BLOCK", "reason":
1429
+ "depth_limit_exceeded"}`. Tool call arguments nested deeper than the JSON
1430
+ parser reads are scanned as they are, and the request is blocked the same
1431
+ way.
1432
+
1433
+ `/mcp` and `/api/v1/validate` scan and redact to the same depth. With the
1434
+ firewall or PII redaction on, a request holding a string, or a non-empty
1435
+ object or array, past level 32 is blocked the same way, since that text
1436
+ would be neither scanned nor redacted. `content` of `/api/v1/validate` must
1437
+ be a string.
1438
+
1439
+ #### Scan scope
1440
+
1441
+ The firewall of the gateway scans the messages whose role is in
1442
+ `ADMINA_GATEWAY_SCAN_ROLES` (comma-separated, among `system`, `user`,
1443
+ `assistant` and `tool`; default all four). Messages with any other role, or
1444
+ none, are always scanned. The scan scope applies to the firewall only: PII
1445
+ redaction and governance guards still see every message.
1446
+
1447
+ A caller that has already scanned part of a prompt, for example retrieved
1448
+ documents scanned with the SDK, can narrow the scan of one request with the
1449
+ `X-Admina-Scan-Policy` header, once the operator has turned scan policies on
1450
+ with `ADMINA_GATEWAY_SCAN_POLICY_ENABLED=true` (default `false`: the header is
1451
+ ignored and every request is scanned in full):
1452
+
1453
+ ```
1454
+ X-Admina-Scan-Policy: v1; roles=user,tool; prescanned=source,document; ruleset=<sha256>
1455
+ ```
1456
+
1457
+ | Field | Meaning |
1458
+ |---|---|
1459
+ | `v1` | format version (required, first) |
1460
+ | `roles` | scan only these of the configured roles (optional) |
1461
+ | `prescanned` | skip the text of `<tag …>…</tag>` blocks of these tags (optional); only tags listed in `gateway.prescan_tags` of `admina.yaml` are skipped |
1462
+ | `ruleset` | `ruleset_sha256()` of the rules the caller scanned with (required) |
1463
+
1464
+ The policy applies only when `ruleset` is the proxy's own ruleset or one listed
1465
+ in `gateway.prescan_rulesets`:
1466
+
1467
+ ```yaml
1468
+ gateway:
1469
+ prescan_tags: [source, document]
1470
+ prescan_rulesets: ["<sha256 of the caller's rules>"]
1471
+ ```
1472
+
1473
+ Otherwise, or when the header is malformed (unknown version or field, duplicate
1474
+ field, unknown role, invalid tag name or ruleset, more than one header), the
1475
+ request is scanned in full, never refused. A block is skipped only when each
1476
+ of its tags pairs up (an opening tag followed by its closing tag); an unclosed,
1477
+ nested or stray tag leaves the whole text to the scan. Tag names are
1478
+ case-sensitive. The caller must keep these tags out of text written by
1479
+ untrusted parties, because the gateway cannot tell such text from its own
1480
+ blocks.
1481
+
1482
+ Trust model: with scan policies on, the gateway takes the caller's word for
1483
+ what it has scanned. Any caller that holds the API key can send the header,
1484
+ and the ruleset it must declare is not a secret (it is on every response and
1485
+ on `GET /v1/admina/ruleset`), so such a caller can narrow the scan of its own
1486
+ requests down to leaving out every user message. Turn scan policies on only
1487
+ when every caller that can reach the gateway is a trusted component that
1488
+ scans the text it declares, for example a service in front of the gateway
1489
+ that scans retrieved documents with the SDK and passes on its users' text as
1490
+ `user` messages.
1491
+
1492
+ The `gateway_request` forensic record carries the outcome:
1493
+
1494
+ ```json
1495
+ "prescan": {"accepted": true, "status": "accepted", "roles": ["user", "tool"],
1496
+ "tags": ["document", "source"], "ruleset": "<sha256>"}
1497
+ ```
1498
+
1499
+ `status` is `none` (no header), `accepted`, `ruleset_mismatch`, `malformed` or
1500
+ `ignored` (scan policies off); `roles` and `tags` are what was applied,
1501
+ `ruleset` what the header declared (`null` when it was ignored). `/metrics`
1502
+ counts the policies: `admina_prescan_accepted_total`,
1503
+ `admina_prescan_ruleset_mismatch_total`, `admina_prescan_malformed_total` and
1504
+ `admina_prescan_ignored_total`.
1505
+
1506
+ #### Governance outcome
1507
+
1508
+ Once a request has passed the route, JSON, model, forwarded value and size
1509
+ checks it gets an event id, and every response to it carries the outcome of governance,
1510
+ streaming or not (response headers, sent before the first event). The body
1511
+ must be a JSON object; any other body is answered `400` (`Invalid JSON body`)
1512
+ before the event id exists.
1513
+
1514
+ | Header | Value |
1515
+ |---|---|
1516
+ | `X-Admina-Event-Id` | the `event_id` of the call's forensic records (32 hex characters); the upstream receives it too |
1517
+ | `X-Admina-Action` | `ALLOW` or `BLOCK` |
1518
+ | `X-Admina-Would-Action` | only in `observe` and `dry-run` mode, when governance would have blocked: `BLOCK` (`X-Admina-Action` is then `ALLOW`) |
1519
+ | `X-Admina-Risk` | `LOW`, `MEDIUM`, `HIGH` or `CRITICAL` |
1520
+ | `X-Admina-Categories` | the names of the firewall categories that matched, comma-separated (for example `instruction_override,prompt_extraction`); empty when none. Never text |
1521
+ | `X-Admina-Record-Hash` | the `record_hash` of the `gateway_request` record, written before the request is forwarded (64 hex characters) |
1522
+
1523
+ Allowed and blocked requests, upstream errors (with their status), timeouts
1524
+ (504), connection failures (502) and failures in the gateway all carry them.
1525
+ A request body with a value JSON cannot encode for the upstream request (a
1526
+ number that is not finite, such as `NaN`, or an unpaired surrogate) is
1527
+ answered `400` with `"code": "invalid_request_body"`; any other failure in
1528
+ the gateway `500` with `"type": "server_error"`. `X-Admina-Ruleset` (see
1529
+ [Firewall ruleset](#firewall-ruleset)) and `X-Admina-Version` (the Admina
1530
+ version, `admina.__version__`) are on these responses and on those the
1531
+ gateway sends before the event id exists (unknown route, invalid JSON,
1532
+ model outside the allowlist, value refused by a forwarding limit, message
1533
+ text over the limit). Read the outcome
1534
+ from `X-Admina-Action`, not from the body.
1535
+
1536
+ `ADMINA_GATEWAY_BLOCK_STATUS` sets how a blocked request is answered:
1537
+
1538
+ | Value | Response |
1539
+ |---|---|
1540
+ | `200` (default) | a completion carrying `ADMINA_GATEWAY_BLOCK_MESSAGE` with `finish_reason: "content_filter"`; for `stream: true`, one SSE chunk and `data: [DONE]` |
1541
+ | `403` | `{"error": {"message": "<ADMINA_GATEWAY_BLOCK_MESSAGE>", "type": "governance_blocked", "param": null, "code": "governance_blocked", "categories": ["instruction_override"]}}`, as JSON, streaming or not |
1542
+
1543
+ The same applies to a non-streaming completion blocked by the response scan,
1544
+ or whose PII redaction did not finish.
1545
+
1546
+ #### Correlation and forensic records
1547
+
1548
+ | Setting | Default | Meaning |
1549
+ |---|---|---|
1550
+ | `ADMINA_GATEWAY_REQUEST_ID_HEADER` | empty | header recorded as `request_id`; empty: `request_id` is `null` |
1551
+ | `ADMINA_GATEWAY_RECORD_HEADERS` | empty | headers recorded in `context` (lower-case name to value); others never are |
1552
+ | `ADMINA_GATEWAY_FORWARD_HEADERS` | empty | headers forwarded upstream |
1553
+
1554
+ ```bash
1555
+ ADMINA_GATEWAY_REQUEST_ID_HEADER=X-Request-Id
1556
+ ADMINA_GATEWAY_RECORD_HEADERS=X-Request-Id,X-Example-Purpose,X-Example-Client
1557
+ ADMINA_GATEWAY_FORWARD_HEADERS=traceparent,tracestate,X-Request-Id
1558
+ ```
1559
+
1560
+ Header names are case-insensitive. Recorded values lose CR and LF and keep at
1561
+ most 128 characters. The proxy does not start when a setting names an invalid
1562
+ header, or a credential (`Authorization`, `Proxy-Authorization`, `Cookie`,
1563
+ `X-API-Key`); the forward list cannot name connection or body headers
1564
+ (`Host`, `Content-Length`, `Transfer-Encoding`, …) or `X-Admina-*` either.
1565
+ The upstream receives the listed headers, the route's `Authorization` and
1566
+ `X-Admina-Event-Id`, and nothing else from the client; a listed header value
1567
+ outside ASCII is forwarded as the bytes received. `X-Session-Id` and
1568
+ `X-Agent-Id` are still recorded as `session_id` and `agent_id`.
1569
+
1570
+ **W3C trace context.** A valid `traceparent` (one header; lowercase hex; not
1571
+ version `ff`; non-zero trace and parent ids; nothing after the flags in
1572
+ version `00`) is recorded as `trace_id`, and forwarded with `tracestate` when
1573
+ they are listed. An invalid `traceparent` is neither recorded nor forwarded,
1574
+ and neither is `tracestate`. With OpenTelemetry on (the `telemetry` extra),
1575
+ each call has a span, `gateway.chat.completions`, a child of the caller's span
1576
+ (or the root of a new trace), with `admina.event_id`, `admina.upstream`,
1577
+ `admina.action`, `http.response.status_code` and, when they apply,
1578
+ `admina.cancelled` and `error.type`; the upstream then receives a
1579
+ `traceparent` naming this span, and `trace_id` is the span's trace.
1580
+
1581
+ Each call writes two forensic records with the same `event_id`. The
1582
+ `gateway_request` record, written before the request is forwarded, carries
1583
+ the governance decision (`action`, `risk_level`, `checks`, `categories`,
1584
+ `would_action` in `observe` and `dry-run` mode), `upstream`, `prescan`,
1585
+ `ruleset_sha256`, `session_id`, `agent_id`, `request_id`, `trace_id`,
1586
+ `context` and `request_sha256`: the SHA-256 of the RFC 8785 (JCS) canonical
1587
+ form of the `messages` array forwarded upstream (`null` when the array has
1588
+ none, for example with an unpaired surrogate, or is nested deeper than the
1589
+ interpreter's recursion limit). Test vectors for other
1590
+ implementations are in `tests/fixtures/jcs_vectors.json`.
1591
+
1592
+ The `gateway_response` record is written once the response has ended: sent
1593
+ whole, left by the client, or failed.
1594
+
1595
+ ```json
1596
+ {
1597
+ "event_id": "<event id>", "event_type": "gateway_response",
1598
+ "request_id": "req-0001", "method": "chat.completions", "upstream": "default",
1599
+ "stream": true, "action": "ALLOW", "status_code": 200, "upstream_status_code": 200,
1600
+ "finish_reason": "stop",
1601
+ "usage": {"prompt_tokens": 9, "completion_tokens": 4, "total_tokens": 13},
1602
+ "duration_ms": 812.4, "response_sha256": "<64 hex>",
1603
+ "cancelled": false, "error": null
1604
+ }
1605
+ ```
1606
+
1607
+ - `response_sha256`: the SHA-256 of the bytes sent to the client, counted as
1608
+ a stream goes out;
1609
+ - `finish_reason`: of the first choice; `usage`: the numbers of the `usage`
1610
+ object (the last stream chunk that has one, with
1611
+ `stream_options.include_usage`, or the body), `null` when there is none;
1612
+ - `status_code`: the status sent to the client; `upstream_status_code`:
1613
+ `null` when the upstream was not called or did not answer;
1614
+ - `cancelled`: the client went away before the end of the response;
1615
+ - `error`: the class of the exception that ended the upstream exchange (for
1616
+ example `ReadTimeout`), never its message.
1617
+
1618
+ A blocked request, and one whose upstream fails, get their `gateway_response`
1619
+ record too: count `gateway_request` records to count requests. Neither record
1620
+ holds prompt or completion text. Both are chained and hashed as before:
1621
+ `record_hash` is the SHA-256 of `json.dumps(record_without_record_hash,
1622
+ sort_keys=True, default=str)`.
1623
+
1624
+ <a id="compliance-scope"></a>
1625
+
1626
+ <details open>
1627
+ <summary><strong>⚖️ Compliance scope &amp; legal disclaimer</strong> — what Admina does and does not do legally</summary>
1628
+
1629
+ <br>
1630
+
1631
+ > Admina is a self-assessment and defense-in-depth tool. The EU AI Act
1632
+ > gap-analysis and risk classification features are **decision-support
1633
+ > aids, not legal advice**. They do not replace the conformity assessment
1634
+ > required under EU AI Act Art. 43 for high-risk systems, nor the
1635
+ > involvement of a notified body where the regulation requires one.
1636
+ >
1637
+ > **EU AI Act timeline (after the Omnibus VII agreement of 7 May 2026):**
1638
+ > Art. 5 prohibitions in force since 2 February 2025; GPAI obligations
1639
+ > in force since 2 August 2025; Art. 50 transparency for synthetic
1640
+ > content and the new NCII / synthetic-CSAM prohibition apply from
1641
+ > 2 December 2026; **Annex III high-risk obligations from 2 December
1642
+ > 2027** (postponed from 2 Aug 2026); Annex I high-risk from 2 August
1643
+ > 2028 (postponed from 2 Aug 2027). The full machine-readable timeline
1644
+ > ships with Admina as `admina.domains.compliance.eu_ai_act.EU_AI_ACT_DEADLINES`.
1645
+ > See [`MODEL_CARD.md`](https://github.com/admina-org/admina/blob/main/MODEL_CARD.md) for the full scope, limitations,
1646
+ > and known failure modes of every Admina component.
1647
+
1648
+ </details>
1649
+
1650
+ ## Integrations
1651
+
1652
+ <details>
1653
+ <summary><strong>GuardrailsAI</strong> — ML-based content validation as a governance plugin</summary>
1654
+
1655
+ <br>
1656
+
1657
+ ML-based content validation (toxic language, jailbreak, bias, PII via Presidio) as a governance domain plugin:
1658
+
1659
+ ```bash
1660
+ # Upstream guardrails-ai is currently in PyPI quarantine. Install it
1661
+ # manually from your local mirror or wheel cache; once available, the
1662
+ # plugin in admina/plugins/builtin/guards/guardrailsai_guard.py will
1663
+ # detect it automatically.
1664
+ pip install <your-guardrails-ai-wheel>
1665
+ ```
1666
+
1667
+ Enable in `admina.yaml` under `agent_security.domains.guardrailsai`. All inference runs locally by default — no data leaves the deployment perimeter.
1668
+
1669
+ </details>
1670
+
1671
+ <details>
1672
+ <summary><strong>OpenClaw</strong> — govern OpenClaw agent actions via pre/post-action hooks</summary>
1673
+
1674
+ <br>
1675
+
1676
+ Govern OpenClaw agent actions through the Admina proxy. Every tool call, shell command, and API request is validated before execution:
1677
+
1678
+ ```bash
1679
+ cd integrations/openclaw/admina-governance
1680
+ chmod +x setup.sh && ./setup.sh
1681
+ ```
1682
+
1683
+ The skill uses `POST /api/v1/validate` (pre-action) and `POST /api/v1/audit` (post-action) endpoints.
1684
+
1685
+ </details>
1686
+
1687
+ <details>
1688
+ <summary><strong>n8n</strong> — community nodes for n8n workflow automation</summary>
1689
+
1690
+ <br>
1691
+
1692
+ | Node | Purpose |
1693
+ |------|---------|
1694
+ | **Admina Govern** | Inline governance check — validates workflow data, blocks injections, redacts PII |
1695
+ | **Admina Audit** | Logs workflow events to forensic black box with EU AI Act risk classification |
1696
+ | **Admina Dashboard** | Trigger node — fires on governance events via WebSocket |
1697
+
1698
+ Install: `npm install n8n-nodes-admina` in your n8n instance.
1699
+
1700
+ </details>
1701
+
1702
+ <details>
1703
+ <summary><strong>Cheshire Cat AI</strong> — govern all Cheshire Cat interactions via Python hooks</summary>
1704
+
1705
+ <br>
1706
+
1707
+ Three Python hooks (`agent_fast_reply`, `before_cat_sends_message`, `before_cat_recalls_memories`):
1708
+
1709
+ ```bash
1710
+ cd integrations/cheshirecat/admina-plugin
1711
+ ./setup.sh # Start Admina sidecar
1712
+ # Copy plugin into Cheshire Cat plugins/ directory
1713
+ ```
1714
+
1715
+ </details>
1716
+
1717
+ <details>
1718
+ <summary><strong>LangChain</strong> — drop-in callback handler</summary>
1719
+
1720
+ <br>
1721
+
1722
+ Governs every LLM call and tool invocation in-process:
1723
+
1724
+ ```python
1725
+ from admina.integrations.langchain.callbacks import AdminaCallbackHandler
1726
+
1727
+ handler = AdminaCallbackHandler()
1728
+ llm = ChatOpenAI(callbacks=[handler])
1729
+ ```
1730
+
1731
+ </details>
1732
+
1733
+ <details>
1734
+ <summary><strong>CrewAI</strong> — step and task callbacks for multi-agent governance</summary>
1735
+
1736
+ <br>
1737
+
1738
+ ```python
1739
+ from admina.integrations.crewai.callbacks import admina_step_callback, admina_task_callback
1740
+
1741
+ agent = Agent(role="Researcher", step_callback=admina_step_callback)
1742
+ crew = Crew(agents=[agent], tasks=[task], task_callback=admina_task_callback)
1743
+ ```
1744
+
1745
+ </details>
1746
+
1747
+ See [full integration docs](https://github.com/admina-org/admina/blob/main/docs/guides/integrations.md) for details.
1748
+
1749
+ ## Performance — Hybrid Python + Rust engine
1750
+
1751
+ The Rust core engine is an optional accelerator. The default
1752
+ `pip install admina-framework` ships only the pure-Python implementation;
1753
+ enable the Rust engine with the opt-in extra `pip install
1754
+ "admina-framework[rust]"` (or build from source for local development —
1755
+ `maturin develop --release --manifest-path core-rust/Cargo.toml`, see
1756
+ [CONTRIBUTING.md](https://github.com/admina-org/admina/blob/main/CONTRIBUTING.md)). Under `ADMINA_ENGINE=auto` Admina
1757
+ runs the Rust firewall and loop breaker when the extension is installed and
1758
+ the Python ones otherwise; `ADMINA_ENGINE=rust` without it stops the proxy
1759
+ (see [Engine selection](#engine-selection)).
1760
+
1761
+ > **Detection trade-off (why Rust is opt-in, not the default).** The Rust
1762
+ > firewall is faster but currently detects a narrower set of attacks than
1763
+ > the pure-Python firewall. The Python engine normalises common evasions
1764
+ > before matching (homoglyph, leetspeak, char-by-char hyphenation, base64,
1765
+ > ROT13) and carries a wider multilingual pattern set; the Rust engine does
1766
+ > not yet. On an internal 14-attack evasion corpus the Python firewall
1767
+ > blocks all 14 while the Rust firewall blocks 7 (the plain-text and
1768
+ > multilingual-keyword attacks), with no false positives on either side.
1769
+ > Keep the Python engine (no `[rust]` extra, or `ADMINA_ENGINE=python`) when
1770
+ > detection breadth matters; use the Rust engine when latency dominates.
1771
+
1772
+ The numbers below are a microbenchmark of the Rust engine components on
1773
+ short inputs (`tests/test_benchmark_14us.py`, run with `pytest -m benchmark`;
1774
+ Apple M4 Max in a Docker Desktop VM, Python 3.11, 10 000 iterations). They
1775
+ are the cost of each engine call on a short text, not the latency the proxy
1776
+ adds: that grows with the length of the text scanned, the Python engine
1777
+ costs more, and the gateway adds its own work. Measure your deployment with
1778
+ `scripts/bench_gateway.py` (below).
1779
+
1780
+ ```
1781
+ Component Rust (median) P95 P99
1782
+ ----------------- ------------- --------- ---------
1783
+ Firewall (regex) 2.08us 2.33us 2.50us
1784
+ PII Scanner 0.62us 0.67us 0.71us
1785
+ Loop Breaker 2.38us 2.67us 2.75us
1786
+ Hash Chain 1.00us 1.12us 1.25us
1787
+ ----------------- ------------- --------- ---------
1788
+ 4-Domain pipeline 6.25us 7.04us 7.29us
1789
+ ```
1790
+
1791
+ <details>
1792
+ <summary>Rust vs Python comparison (click to expand)</summary>
1793
+
1794
+ ```
1795
+ Component Python (median) Rust (median) Speedup
1796
+ ----------------- --------------- ------------- --------
1797
+ Firewall 7.79us 2.08us 3.7x
1798
+ PII (regex-only) 8.21us 0.62us 13.2x
1799
+ PII (with spaCy) 1 992us 0.62us 3 213x
1800
+ Loop (sklearn) 505us 2.38us 212x
1801
+ ----------------- --------------- ------------- --------
1802
+ Full pipeline 2 261us 5.21us 434x
1803
+ ```
1804
+
1805
+ </details>
1806
+
1807
+ `scripts/bench_gateway.py` measures the latency the OpenAI-compatible gateway
1808
+ adds on a retrieval-augmented trace, in one process: a mock upstream streaming
1809
+ 1000 chunks 5 ms apart, and a prompt with 12 `<source>` blocks of generated
1810
+ prose (about 44,000 characters). It reports the time to the first chunk,
1811
+ direct and through the gateway with and without `X-Admina-Scan-Policy`, at 1
1812
+ and 8 concurrent requests, and the event loop lag with 8 clients streaming,
1813
+ for each firewall engine:
1814
+
1815
+ ```bash
1816
+ .venv/bin/python scripts/bench_gateway.py --engines python,rust --json bench.json
1817
+ ```
1818
+
1819
+ ## Traffic Simulator
1820
+
1821
+ Generate realistic governance traffic to test and demo the platform:
1822
+
1823
+ ```bash
1824
+ # Start the proxy
1825
+ docker compose up -d
1826
+
1827
+ # Default: 60s at 2 req/s
1828
+ python scripts/simulate.py
1829
+
1830
+ # Intense: 5 minutes at 10 req/s
1831
+ python scripts/simulate.py --duration 300 --rate 10
1832
+ ```
1833
+
1834
+ Generates a weighted mix of: clean MCP requests, injection attempts, PII content, loop triggers, REST validate/audit calls, EU AI Act classifications, and dashboard reads. Colored terminal output with per-event action and summary counters.
1835
+
1836
+ ## Infrastructure & Services
1837
+
1838
+ The full stack (`docker compose up`) runs 8 containers:
1839
+
1840
+ | Port | Service | Description |
1841
+ |------|---------|-------------|
1842
+ | `8080` | Proxy | MCP proxy + REST API + OpenAPI docs |
1843
+ | `3000` | Dashboard | Real-time governance web UI (`127.0.0.1` only) |
1844
+ | `3001` | Grafana | Metrics dashboards |
1845
+ | `4317` | OTEL Collector | OTLP gRPC ingestion |
1846
+
1847
+ ClickHouse and Redis are internal only (not exposed to host).
1848
+
1849
+ <details>
1850
+ <summary><strong>🗄️ Forensic backends (4 options) — choose deliberately</strong></summary>
1851
+
1852
+ <br>
1853
+
1854
+ The forensic blackbox (the SHA-256 hash chain that makes the audit trail
1855
+ tamper-evident) supports three backends. Read this before picking one for
1856
+ production.
1857
+
1858
+ | Backend | License | When to use | Caveats |
1859
+ |---------|---------|-------------|---------|
1860
+ | **`memory`** *(default)* | n/a | Local development, tests, demos | Records are LOST on restart — no audit persistence. Loud warning at startup. |
1861
+ | **`filesystem`** | n/a | Single-host on-prem, air-gapped, smaller deployments | Persistence depends on the host filesystem; not ideal for HA. Requires `FORENSIC_BASE_DIR`. |
1862
+ | **`s3`** *(boto3)* | Apache 2.0 (boto3) | Production / HA / multi-region | Works with **any S3-compatible service** — AWS S3, **MinIO** servers, Cloudflare R2, Backblaze B2, **SeaweedFS** (Apache 2.0), **Garage** (AGPLv3), **Ceph RGW** (LGPLv2). Supports WORM Object Lock. |
1863
+
1864
+ > **Using MinIO?** Point the `s3` backend at your MinIO server via
1865
+ > `FORENSIC_S3_ENDPOINT` — MinIO speaks the S3 API, so no MinIO-specific
1866
+ > client is needed. The legacy `minio`-SDK backend was removed in 0.9.5
1867
+ > (the MinIO Python SDK is archived); `FORENSIC_BACKEND=minio` now
1868
+ > transparently routes to the `s3` backend with a migration warning.
1869
+
1870
+ </details>
1871
+
1872
+ <details>
1873
+ <summary><strong>⚙️ Environment variables (Docker / .env)</strong></summary>
1874
+
1875
+ <br>
1876
+
1877
+ | Variable | Default | Description |
1878
+ |----------|---------|-------------|
1879
+ | `ADMINA_API_KEY` | *(empty)* | API key for all endpoints |
1880
+ | `ADMINA_API_KEY_FILE` | *(empty)* | File holding the API key, instead of `ADMINA_API_KEY` |
1881
+ | `ADMINA_AUDIT_APPEND_KEY` | *(empty)* | Key accepted by `POST /api/v1/audit` only (also `_FILE`); empty: the route needs the API key |
1882
+ | `ADMINA_CONFIG` | *(empty)* | `admina.yaml` to load (empty: current directory, then package directory) |
1883
+ | `ADMINA_ENABLED_SURFACES` | *(empty = all)* | Surfaces served: `gateway`, `mcp`, `integration`, `compliance`, `dashboard` |
1884
+ | `UPSTREAM_MCP_URL` | `http://localhost:9000` | Default upstream MCP server |
1885
+ | `REDIS_URL` | `redis://localhost:6379/0` | Session state + rate limiting (empty = no Redis) |
1886
+ | `CLICKHOUSE_HOST` | `localhost` | Event analytics (empty = no ClickHouse) |
1887
+ | `FORENSIC_BACKEND` | `memory` | Forensic store: `memory` \| `filesystem` \| `s3` (else `domains.compliance.forensic.backend` of `admina.yaml`) |
1888
+ | `FORENSIC_BASE_DIR` | *(empty)* | Directory of the `filesystem` store (else `domains.compliance.forensic.base_dir`) |
1889
+ | `ADMINA_FORENSIC_FAIL_MODE` | `open` | A forensic record that cannot be written: `open` (logged, request served) \| `closed` (`503`, not forwarded) |
1890
+ | `LOG_LEVEL` | `INFO` | Logging verbosity |
1891
+ | `ADMINA_LOG_FORMAT` | `text` | Log output: `text` \| `json` |
1892
+
1893
+ </details>
1894
+
1895
+ <details>
1896
+ <summary><strong>📁 Full project structure</strong></summary>
1897
+
1898
+ <br>
1899
+
1900
+ ```
1901
+ admina/
1902
+ +-- admina/ SDK package (GovernedModel, GovernedData, GovernedAgent, ComplianceKit)
1903
+ | +-- plugins/ Plugin base classes + registry
1904
+ +-- domains/ 4 governance domains
1905
+ | +-- data_sovereignty/ PII, residency, classification
1906
+ | +-- ai_infra/ LLM engine, RAG pipeline, Web UI
1907
+ | +-- agent_security/ Firewall, loop breaker, proxy
1908
+ | +-- compliance/ EU AI Act, forensic, OTEL
1909
+ +-- plugins/builtin/ Reference plugin implementations
1910
+ | +-- adapters/ Ollama, OpenAI
1911
+ | +-- connectors/ ChromaDB, Filesystem
1912
+ | +-- domains/ GuardrailsAI
1913
+ | +-- compliance/ EU AI Act template
1914
+ | +-- transports/ MCP, HTTP REST
1915
+ | +-- forensic/ Filesystem
1916
+ | +-- auth/ API Key
1917
+ | +-- pii/ spaCy + Regex
1918
+ | +-- alerts/ Log, Webhook
1919
+ +-- proxy/ FastAPI proxy + Rust engine bridge
1920
+ | +-- api/ Dashboard + integration REST endpoints
1921
+ +-- cli/ CLI commands (init, dev, plugin)
1922
+ +-- core/ Config, types, event bus
1923
+ +-- core-rust/ Rust governance engines (PyO3)
1924
+ +-- dashboard/ Real-time governance web UI
1925
+ +-- integrations/
1926
+ | +-- openclaw/ OpenClaw governance skill
1927
+ | +-- n8n/ n8n community nodes
1928
+ +-- tests/ 800+ tests (pytest)
1929
+ +-- docker-compose.yml Full stack deployment (8 containers)
1930
+ ```
1931
+
1932
+ </details>
1933
+
1934
+ <details>
1935
+ <summary><strong>🔌 API examples (curl)</strong></summary>
1936
+
1937
+ <br>
1938
+
1939
+ ```bash
1940
+ # Health check (always public)
1941
+ curl http://localhost:8080/health
1942
+
1943
+ # Governance stats
1944
+ curl http://localhost:8080/api/stats -H "X-API-Key: $ADMINA_API_KEY"
1945
+
1946
+ # Proxy an MCP call (all governance domains applied)
1947
+ curl -X POST http://localhost:8080/mcp \
1948
+ -H "Content-Type: application/json" \
1949
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}'
1950
+
1951
+ # Validate content (REST API for integrations)
1952
+ curl -X POST http://localhost:8080/api/v1/validate \
1953
+ -H "Content-Type: application/json" \
1954
+ -d '{"content": "Check this text for governance issues"}'
1955
+
1956
+ # Audit an action (forensic logging)
1957
+ curl -X POST http://localhost:8080/api/v1/audit \
1958
+ -H "Content-Type: application/json" \
1959
+ -d '{"event": {"action": "llm_call", "status": "success"}}'
1960
+
1961
+ # EU AI Act risk classification
1962
+ curl -X POST http://localhost:8080/api/compliance/classify \
1963
+ -H "Content-Type: application/json" \
1964
+ -d '{"description":"AI credit scoring","use_case":"lending","data_types":["financial"]}'
1965
+
1966
+ # Dashboard governance score
1967
+ curl http://localhost:8080/api/dashboard/score
1968
+ ```
1969
+
1970
+ </details>
1971
+
1972
+ ## Project documents
1973
+
1974
+ - [CONTRIBUTING.md](https://github.com/admina-org/admina/blob/main/CONTRIBUTING.md) — development setup, testing, and pull request workflow
1975
+ - [MODEL_CARD.md](https://github.com/admina-org/admina/blob/main/MODEL_CARD.md) — transparency artifact for every Admina governance component (intended use, scope, limitations, known failure modes), aligned with EU AI Act Art. 13 and NIST AI RMF
1976
+ - [ROADMAP.md](https://github.com/admina-org/admina/blob/main/ROADMAP.md) — planned milestones from 0.9.x to 1.0 and beyond
1977
+ - [CHANGELOG.md](https://github.com/admina-org/admina/blob/main/CHANGELOG.md) — release notes
1978
+ - [SECURITY.md](https://github.com/admina-org/admina/blob/main/SECURITY.md) — coordinated disclosure policy
1979
+ - [CODE_OF_CONDUCT.md](https://github.com/admina-org/admina/blob/main/CODE_OF_CONDUCT.md) — Contributor Covenant 2.1
1980
+ - **Browse the AI-generated wiki** → [deepwiki.com/admina-org/admina](https://deepwiki.com/admina-org/admina)
1981
+
1982
+ Admina is Apache 2.0. Contributions are welcome.
1983
+
1984
+ ## License
1985
+
1986
+ Copyright © 2025–2026 [Stefano Noferi](https://github.com/stefanoferi) & Admina contributors
1987
+
1988
+ Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/admina-org/admina/blob/main/LICENSE) for the full text.
1989
+
1990
+ ---
1991
+
1992
+ <p align="center">
1993
+ <img src="https://admina.org/admina-heimdall-the-governance-owl.png" alt="Heimdall — the Governance Owl" width="80" /><br/>
1994
+ <em>Heimdall — the Governance Owl</em><br/><br/>
1995
+ <strong>admina.org</strong> · Created by Stefano Noferi · Pisa, Italy
1996
+ </p>