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