admina-framework 0.12.2__tar.gz → 0.13.1__tar.gz

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