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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {admina_framework-0.12.0/admina_framework.egg-info → admina_framework-0.12.2}/PKG-INFO +5 -1
  2. {admina_framework-0.12.0 → admina_framework-0.12.2}/README.md +4 -0
  3. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/__init__.py +1 -1
  4. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/main.py +1 -0
  5. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/index.html +156 -7
  6. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/forensic.py +21 -0
  7. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/engines/presidio.py +6 -1
  8. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/auth/apikey.py +10 -47
  9. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/dashboard.py +213 -41
  10. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/gateway.py +165 -4
  11. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/config.py +16 -4
  12. admina_framework-0.12.2/admina/proxy/dashboard_session.py +133 -0
  13. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/main.py +191 -81
  14. {admina_framework-0.12.0 → admina_framework-0.12.2/admina_framework.egg-info}/PKG-INFO +5 -1
  15. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/SOURCES.txt +2 -0
  16. {admina_framework-0.12.0 → admina_framework-0.12.2}/pyproject.toml +1 -1
  17. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_forensic_blackbox.py +41 -0
  18. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_endpoints.py +331 -0
  19. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_mount.py +71 -0
  20. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_presidio_engine.py +27 -0
  21. admina_framework-0.12.2/tests/test_proxy_dashboard_session.py +800 -0
  22. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_security.py +25 -72
  23. {admina_framework-0.12.0 → admina_framework-0.12.2}/LICENSE +0 -0
  24. {admina_framework-0.12.0 → admina_framework-0.12.2}/NOTICE +0 -0
  25. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/__init__.py +0 -0
  26. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/commands/__init__.py +0 -0
  27. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/admina.yaml.j2 +0 -0
  28. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/docker-compose.yml.j2 +0 -0
  29. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/env.j2 +0 -0
  30. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/main.py.j2 +0 -0
  31. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin.py.j2 +0 -0
  32. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_pyproject.toml.j2 +0 -0
  33. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_readme.md.j2 +0 -0
  34. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/cli/templates/plugin_test.py.j2 +0 -0
  35. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/__init__.py +0 -0
  36. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/config.py +0 -0
  37. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/event_bus.py +0 -0
  38. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/secrets.py +0 -0
  39. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/core/types.py +0 -0
  40. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/__init__.py +0 -0
  41. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/heimdall.png +0 -0
  42. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
  43. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/__init__.py +0 -0
  44. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/__init__.py +0 -0
  45. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/coordination.py +0 -0
  46. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/egress.py +0 -0
  47. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/fingerprint.py +0 -0
  48. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/firewall.py +0 -0
  49. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/agent_security/loop_breaker.py +0 -0
  50. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/__init__.py +0 -0
  51. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/llm_engine.py +0 -0
  52. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/rag.py +0 -0
  53. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/ai_infra/webui.py +0 -0
  54. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/__init__.py +0 -0
  55. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/cross_regulation.py +0 -0
  56. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/eu_ai_act.py +0 -0
  57. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/gdpr.py +0 -0
  58. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/nis2.py +0 -0
  59. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/oisg.py +0 -0
  60. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/compliance/otel.py +0 -0
  61. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/__init__.py +0 -0
  62. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/classification.py +0 -0
  63. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/pii.py +0 -0
  64. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/data_sovereignty/residency.py +0 -0
  65. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/domains/governance.py +0 -0
  66. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/engines/__init__.py +0 -0
  67. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/__init__.py +0 -0
  68. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/_engines.py +0 -0
  69. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/cheshirecat/__init__.py +0 -0
  70. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
  71. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/crewai/__init__.py +0 -0
  72. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/crewai/callbacks.py +0 -0
  73. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/langchain/__init__.py +0 -0
  74. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/langchain/callbacks.py +0 -0
  75. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/n8n/__init__.py +0 -0
  76. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/integrations/openclaw/__init__.py +0 -0
  77. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/__init__.py +0 -0
  78. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/base.py +0 -0
  79. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/__init__.py +0 -0
  80. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/__init__.py +0 -0
  81. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/_streaming.py +0 -0
  82. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/anthropic.py +0 -0
  83. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/bedrock.py +0 -0
  84. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/gemini.py +0 -0
  85. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/mistral.py +0 -0
  86. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/ollama.py +0 -0
  87. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/openai.py +0 -0
  88. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/adapters/vllm.py +0 -0
  89. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/__init__.py +0 -0
  90. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/log.py +0 -0
  91. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/alerts/webhook.py +0 -0
  92. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/auth/__init__.py +0 -0
  93. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/compliance/__init__.py +0 -0
  94. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/compliance/eu_ai_act.py +0 -0
  95. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/__init__.py +0 -0
  96. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/chromadb.py +0 -0
  97. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/connectors/filesystem.py +0 -0
  98. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/forensic/__init__.py +0 -0
  99. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/forensic/filesystem.py +0 -0
  100. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/guards/__init__.py +0 -0
  101. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/guards/guardrailsai_guard.py +0 -0
  102. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/pii/__init__.py +0 -0
  103. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/pii/spacy_regex.py +0 -0
  104. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/__init__.py +0 -0
  105. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/http_rest.py +0 -0
  106. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/builtin/transports/mcp.py +0 -0
  107. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/plugins/registry.py +0 -0
  108. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/__init__.py +0 -0
  109. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/__init__.py +0 -0
  110. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/api/integration.py +0 -0
  111. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/engine_bridge.py +0 -0
  112. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/governance.py +0 -0
  113. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/multi_upstream.py +0 -0
  114. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/proxy/state.py +0 -0
  115. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/py.typed +0 -0
  116. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/__init__.py +0 -0
  117. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/baselines/baseline.json +0 -0
  118. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/SHA256SUMS +0 -0
  119. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/coordination.jsonl +0 -0
  120. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/injection.jsonl +0 -0
  121. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/loop.jsonl +0 -0
  122. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora/pii.jsonl +0 -0
  123. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/corpora.py +0 -0
  124. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/detectors.py +0 -0
  125. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/gate.py +0 -0
  126. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/metrics.py +0 -0
  127. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/report.py +0 -0
  128. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/redteam/runner.py +0 -0
  129. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/__init__.py +0 -0
  130. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/_compat.py +0 -0
  131. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/compliance_kit.py +0 -0
  132. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/errors.py +0 -0
  133. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_agent.py +0 -0
  134. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_data.py +0 -0
  135. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/governed_model.py +0 -0
  136. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/retry.py +0 -0
  137. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina/sdk/streaming.py +0 -0
  138. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/dependency_links.txt +0 -0
  139. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/entry_points.txt +0 -0
  140. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/requires.txt +0 -0
  141. {admina_framework-0.12.0 → admina_framework-0.12.2}/admina_framework.egg-info/top_level.txt +0 -0
  142. {admina_framework-0.12.0 → admina_framework-0.12.2}/setup.cfg +0 -0
  143. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_benchmark_14us.py +0 -0
  144. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination.py +0 -0
  145. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_cli.py +0 -0
  146. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_corpus.py +0 -0
  147. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_coordination_wiring.py +0 -0
  148. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_core_config.py +0 -0
  149. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_domains.py +0 -0
  150. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_domains_governance.py +0 -0
  151. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_analyze.py +0 -0
  152. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_cli.py +0 -0
  153. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_pipeline.py +0 -0
  154. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_policy.py +0 -0
  155. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_egress_surfaces.py +0 -0
  156. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_engines.py +0 -0
  157. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_error_disclosure.py +0 -0
  158. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_fingerprint.py +0 -0
  159. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_firewall_parity.py +0 -0
  160. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_config.py +0 -0
  161. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_guard_fail_mode.py +0 -0
  162. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_gateway_helpers.py +0 -0
  163. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_governance_pipeline.py +0 -0
  164. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_guard_fail_mode.py +0 -0
  165. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_mcp_transport.py +0 -0
  166. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_forensic_would_action.py +0 -0
  167. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_governance_mode_config.py +0 -0
  168. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_guard_fail_mode.py +0 -0
  169. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_mcp_response_pii.py +0 -0
  170. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_metrics_endpoint.py +0 -0
  171. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_proxy_plugins.py +0 -0
  172. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_redteam_efficacy.py +0 -0
  173. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_redteam_lib.py +0 -0
  174. {admina_framework-0.12.0 → admina_framework-0.12.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: admina-framework
3
- Version: 0.12.0
3
+ Version: 0.12.2
4
4
  Summary: Admina — governed AI development framework
5
5
  Author-email: Stefano Noferi <info@admina.org>
6
6
  Maintainer-email: Stefano Noferi <info@admina.org>
@@ -382,6 +382,10 @@ Real-time governance dashboard on port 3000:
382
382
 
383
383
  API backend: `GET /api/dashboard/score`, `/feed`, `/compliance`, `/sovereignty`, `/infra`, `/models`
384
384
 
385
+ In `admina dev` local mode the dashboard asks for the API key (`admina password show`)
386
+ and keeps a short-lived browser session that only the read-only dashboard API accepts.
387
+ Set `ADMINA_DASHBOARD_ENABLED=false` to stop serving the bundled dashboard.
388
+
385
389
  ## Configuration
386
390
 
387
391
  Admina uses `admina.yaml` as the primary config file (with `.env` fallback for backward compatibility):
@@ -305,6 +305,10 @@ Real-time governance dashboard on port 3000:
305
305
 
306
306
  API backend: `GET /api/dashboard/score`, `/feed`, `/compliance`, `/sovereignty`, `/infra`, `/models`
307
307
 
308
+ In `admina dev` local mode the dashboard asks for the API key (`admina password show`)
309
+ and keeps a short-lived browser session that only the read-only dashboard API accepts.
310
+ Set `ADMINA_DASHBOARD_ENABLED=false` to stop serving the bundled dashboard.
311
+
308
312
  ## Configuration
309
313
 
310
314
  Admina uses `admina.yaml` as the primary config file (with `.env` fallback for backward compatibility):
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  from admina.sdk import ComplianceKit, GovernedAgent, GovernedData, GovernedModel
25
25
 
26
- __version__ = "0.12.0"
26
+ __version__ = "0.12.2"
27
27
 
28
28
  __all__ = [
29
29
  "__version__",
@@ -643,6 +643,7 @@ def _run_local(
643
643
  click.echo(f" http://{ip}:{port} ({label})")
644
644
  else:
645
645
  click.echo(f" Ready → http://{display_host}:{port}")
646
+ click.echo(" Dashboard sign-in: the API key (show it with `admina password show`)")
646
647
  if not no_browser:
647
648
  webbrowser.open(f"http://{display_host}:{port}")
648
649
  else:
@@ -330,6 +330,33 @@
330
330
  .link-card:hover { border-color: var(--teal); }
331
331
  .link-card .lc-label { font-size: 11px; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
332
332
  .link-card .lc-title { font-size: 14px; font-weight: 600; }
333
+
334
+ /* ── Sign-in ─────────────────────────────────────────── */
335
+ .signin {
336
+ position: fixed; inset: 0; z-index: 300; background: var(--bg);
337
+ display: flex; align-items: center; justify-content: center; padding: 24px;
338
+ }
339
+ .signin-card {
340
+ width: 100%; max-width: 420px; background: var(--surface);
341
+ border: 1px solid var(--border2); border-radius: 12px; padding: 28px;
342
+ }
343
+ .signin-card .logo { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 700; margin-bottom: 16px; }
344
+ .signin-card .logo img { width: 32px; height: 32px; object-fit: contain; }
345
+ .signin-card .logo .accent { color: var(--teal); }
346
+ .signin-card p { font-size: 13px; color: var(--muted); line-height: 1.6; margin-bottom: 16px; }
347
+ .signin-card code { font-family: var(--mono); font-size: 12px; color: var(--text); }
348
+ .signin-card label { display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin-bottom: 6px; }
349
+ .signin-card input {
350
+ width: 100%; padding: 9px 12px; border-radius: 6px; border: 1px solid var(--border2);
351
+ background: var(--surface2); color: var(--text); font-family: var(--mono); font-size: 13px;
352
+ }
353
+ .signin-card input:focus { outline: none; border-color: var(--teal); }
354
+ .signin-card button {
355
+ margin-top: 14px; width: 100%; padding: 9px 12px; border-radius: 6px; cursor: pointer;
356
+ background: var(--teal-dark); color: #fff; border: 1px solid var(--teal); font-size: 14px; font-weight: 600;
357
+ }
358
+ .signin-card button:disabled { opacity: 0.6; cursor: default; }
359
+ .signin-card .error { margin-top: 12px; font-size: 13px; color: var(--red); }
333
360
  </style>
334
361
  </head>
335
362
  <body>
@@ -339,6 +366,25 @@
339
366
  ════════════════════════════════════════════════════════ -->
340
367
  <div x-data="dashboard()" x-init="init()">
341
368
 
369
+ <!-- Sign-in: the API key is exchanged for a short-lived browser session -->
370
+ <div class="signin" x-show="needsLogin" x-cloak>
371
+ <form class="signin-card" @submit.prevent="login()">
372
+ <div class="logo">
373
+ <img src="heimdall.png" alt="">
374
+ <span><span class="accent">Admina</span> Governance Dashboard</span>
375
+ </div>
376
+ <p>
377
+ Sign in with this instance's API key. In an <code>admina dev</code> project,
378
+ run <code>admina password show</code> to display it.
379
+ </p>
380
+ <label for="admina-api-key">API key</label>
381
+ <input id="admina-api-key" type="password" x-model="loginKey" x-ref="loginKey"
382
+ autocomplete="off" autocapitalize="off" spellcheck="false" required>
383
+ <button type="submit" :disabled="loginBusy || !loginKey">Sign in</button>
384
+ <div class="error" x-show="loginError" x-text="loginError" role="alert"></div>
385
+ </form>
386
+ </div>
387
+
342
388
  <!-- Top Bar -->
343
389
  <div class="topbar">
344
390
  <div class="logo">
@@ -349,6 +395,9 @@
349
395
  <span class="ts" x-text="lastUpdate"></span>
350
396
  <div class="sep"></div>
351
397
  <button class="refresh-btn" @click="refresh()">Refresh</button>
398
+ <template x-if="sessionActive">
399
+ <button class="refresh-btn" @click="logout()">Sign out</button>
400
+ </template>
352
401
  <div class="sep"></div>
353
402
  <div class="status">
354
403
  <div class="dot" :style="{ background: healthy ? 'var(--green)' : 'var(--red)' }"></div>
@@ -686,12 +735,14 @@
686
735
  </p>
687
736
  <p>
688
737
  Until you submit a real assessment, the dashboard renders all articles at 0% with a default
689
- placeholder. To record your real coverage, POST your declared evidence to
690
- <code class="inline">/api/compliance/gap-analysis</code>.
738
+ placeholder. To record your real coverage, send your declared evidence with a
739
+ <strong>POST</strong> request to <code class="inline">/api/compliance/gap-analysis</code>
740
+ on this host (the route accepts POST only: opening it in the browser answers
741
+ 405 Method Not Allowed).
691
742
  </p>
692
743
  <p style="margin-bottom: 6px;"><strong>Example request:</strong></p>
693
744
  <button class="modal-copy-btn" @click="copyAssessmentCmd($event)">Copy command</button>
694
- <pre class="modal-pre" id="assessment-cmd">curl -X POST http://localhost:8080/api/compliance/gap-analysis \
745
+ <pre class="modal-pre" id="assessment-cmd">curl -X POST <span x-text="apiBase"></span>/api/compliance/gap-analysis \
695
746
  -H "X-API-Key: $ADMINA_API_KEY" -H "Content-Type: application/json" \
696
747
  -d '{"risk_category": "high",
697
748
  "current_compliance": {
@@ -807,7 +858,9 @@
807
858
  * Admina Governance Dashboard — Alpine.js application.
808
859
  *
809
860
  * Reads from the Phase 3.1 API endpoints. Does NOT write.
810
- * All API calls go through the nginx reverse proxy (/api/*).
861
+ * All API calls go to /api/* on the same origin: the proxy itself in
862
+ * `admina dev` local mode (browser session after sign-in), or the nginx
863
+ * reverse proxy of the Docker stack (which adds the API key).
811
864
  */
812
865
  function dashboard() {
813
866
  return {
@@ -824,6 +877,12 @@ function dashboard() {
824
877
  modelData: {},
825
878
  oisg: {},
826
879
  showAssessmentHelp: false,
880
+ needsLogin: false,
881
+ sessionActive: false,
882
+ loginKey: '',
883
+ loginError: '',
884
+ loginBusy: false,
885
+ _started: false,
827
886
  _ws: null,
828
887
  _feedId: 0,
829
888
  _refreshTimer: null,
@@ -920,17 +979,96 @@ function dashboard() {
920
979
 
921
980
  // ── Init ───────────────────────────────────────────
922
981
  async init() {
923
- await this.refresh();
982
+ if (await this.checkSession()) this.start();
983
+ },
984
+
985
+ start() {
986
+ // Alpine calls init() both automatically and via x-init: start once.
987
+ if (this._started) return;
988
+ this._started = true;
989
+ this.refresh();
924
990
  this.connectWebSocket();
925
991
  // Auto-refresh every 10s
926
992
  this._refreshTimer = setInterval(() => this.refresh(), 10000);
927
993
  },
928
994
 
995
+ // ── Session ────────────────────────────────────────
996
+ // Serving this page never signs the browser in. The API key is exchanged
997
+ // once for a short-lived HttpOnly session cookie that only the read-only
998
+ // dashboard API accepts. Behind the Docker nginx the proxy already
999
+ // receives the key, so no sign-in is needed there.
1000
+ async checkSession() {
1001
+ try {
1002
+ const r = await fetch('/api/dashboard/session', { cache: 'no-store' });
1003
+ if (r.status === 401) { this.showLogin(''); return false; }
1004
+ if (r.ok) this.sessionActive = !!(await r.json()).session;
1005
+ } catch (e) { /* network error: start anyway and show Disconnected */ }
1006
+ return true;
1007
+ },
1008
+
1009
+ async login() {
1010
+ if (!this.loginKey) return;
1011
+ this.loginBusy = true;
1012
+ this.loginError = '';
1013
+ try {
1014
+ const r = await fetch('/api/dashboard/session', {
1015
+ method: 'POST',
1016
+ headers: { 'X-API-Key': this.loginKey },
1017
+ cache: 'no-store',
1018
+ });
1019
+ if (r.ok) {
1020
+ this.loginKey = '';
1021
+ this.needsLogin = false;
1022
+ this.sessionActive = true;
1023
+ this.start();
1024
+ } else if (r.status === 401) {
1025
+ this.loginError = 'Invalid API key.';
1026
+ } else {
1027
+ let detail = '';
1028
+ try { detail = (await r.json()).detail || ''; } catch (e) { /* not JSON */ }
1029
+ this.loginError = detail || `Sign-in failed (HTTP ${r.status}).`;
1030
+ }
1031
+ } catch (e) {
1032
+ this.loginError = 'Admina is not reachable.';
1033
+ } finally {
1034
+ this.loginBusy = false;
1035
+ }
1036
+ },
1037
+
1038
+ async logout() {
1039
+ try { await fetch('/api/dashboard/session', { method: 'DELETE', cache: 'no-store' }); }
1040
+ catch (e) { /* cookie expires on its own */ }
1041
+ this.showLogin('');
1042
+ },
1043
+
1044
+ showLogin(message) {
1045
+ clearInterval(this._refreshTimer);
1046
+ this._refreshTimer = null;
1047
+ this._started = false;
1048
+ if (this._ws) {
1049
+ const ws = this._ws;
1050
+ this._ws = null;
1051
+ ws.onclose = null;
1052
+ ws.close();
1053
+ }
1054
+ this.wsConnected = false;
1055
+ this.sessionActive = false;
1056
+ this.feedEvents = [];
1057
+ this.loginError = message;
1058
+ this.needsLogin = true;
1059
+ this.$nextTick(() => this.$refs.loginKey && this.$refs.loginKey.focus());
1060
+ },
1061
+
929
1062
  // ── Fetch all data ─────────────────────────────────
930
1063
  async refresh() {
931
1064
  // allSettled so one failing endpoint never blanks the whole dashboard.
932
1065
  const okJson = async (url) => {
933
1066
  const r = await fetch(url);
1067
+ if (r.status === 401) {
1068
+ const err = new Error(`${url} -> 401`);
1069
+ err.unauthorized = true;
1070
+ throw err;
1071
+ }
934
1072
  if (!r.ok) throw new Error(`${url} -> ${r.status}`);
935
1073
  return r.json();
936
1074
  };
@@ -944,6 +1082,10 @@ function dashboard() {
944
1082
  okJson('/api/dashboard/models'),
945
1083
  okJson('/api/dashboard/oisg'),
946
1084
  ]);
1085
+ if (results.some(r => r.status === 'rejected' && r.reason && r.reason.unauthorized)) {
1086
+ this.showLogin('Your session has expired. Sign in again.');
1087
+ return;
1088
+ }
947
1089
  const [scoreRes, compRes, sovRes, statsRes, feedRes, infraRes, modelsRes, oisgRes] =
948
1090
  results.map(r => r.status === 'fulfilled' ? r.value : null);
949
1091
 
@@ -998,8 +1140,9 @@ function dashboard() {
998
1140
 
999
1141
  this._ws.onclose = () => {
1000
1142
  this.wsConnected = false;
1143
+ if (this.needsLogin) return;
1001
1144
  console.log('[Admina] WebSocket disconnected, reconnecting in 3s...');
1002
- setTimeout(() => this.connectWebSocket(), 3000);
1145
+ setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 3000);
1003
1146
  };
1004
1147
 
1005
1148
  this._ws.onerror = () => {
@@ -1007,11 +1150,17 @@ function dashboard() {
1007
1150
  };
1008
1151
  } catch (e) {
1009
1152
  this.wsConnected = false;
1010
- setTimeout(() => this.connectWebSocket(), 5000);
1153
+ setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 5000);
1011
1154
  }
1012
1155
  },
1013
1156
 
1014
1157
  // ── Helpers ─────────────────────────────────────────
1158
+ // The origin that served this page: the proxy itself, or the dashboard
1159
+ // container that forwards /api/ to it.
1160
+ get apiBase() {
1161
+ return window.location.origin;
1162
+ },
1163
+
1015
1164
  copyAssessmentCmd(ev) {
1016
1165
  const pre = document.getElementById('assessment-cmd');
1017
1166
  if (!pre) return;
@@ -24,6 +24,7 @@ import logging
24
24
  import os
25
25
  import threading
26
26
  import time
27
+ from collections import deque
27
28
  from datetime import UTC, datetime
28
29
  from pathlib import Path
29
30
 
@@ -35,6 +36,8 @@ logger = logging.getLogger("admina.forensic_blackbox")
35
36
  _CHAIN_STATE_KEY = "_chain_state.json"
36
37
  # Sidecar holding the HMAC-SHA256 signature of the chain-state payload.
37
38
  _CHAIN_STATE_SIG_KEY = "_chain_state.json.sig"
39
+ # Records kept in memory for recent_records(), newest last.
40
+ RECENT_RECORDS_MAX = 1000
38
41
 
39
42
 
40
43
  class ForensicBlackBox(BaseForensicStore):
@@ -81,6 +84,10 @@ class ForensicBlackBox(BaseForensicStore):
81
84
  self.chain_head: str = "GENESIS"
82
85
  self.record_count: int = 0
83
86
  self._write_lock = threading.Lock()
87
+ # The last records written by this process, for readers that need
88
+ # recent activity without reading the backend back (the dashboard
89
+ # feed when no analytics store is configured).
90
+ self._recent: deque[dict] = deque(maxlen=RECENT_RECORDS_MAX)
84
91
  self._state_signing_key = state_signing_key or os.environ.get("ADMINA_FORENSIC_STATE_KEY")
85
92
  if self.filesystem_dir is not None:
86
93
  self.filesystem_dir.mkdir(parents=True, exist_ok=True)
@@ -346,6 +353,7 @@ class ForensicBlackBox(BaseForensicStore):
346
353
  # Store the record and persist the updated chain state
347
354
  self._store_to_s3(forensic_record)
348
355
  self._persist_chain_state()
356
+ self._recent.append(forensic_record)
349
357
 
350
358
  return {
351
359
  "sequence_number": self.record_count,
@@ -354,6 +362,19 @@ class ForensicBlackBox(BaseForensicStore):
354
362
  "stored": (self.boto3_client is not None or self.filesystem_dir is not None),
355
363
  }
356
364
 
365
+ def recent_records(self, limit: int = RECENT_RECORDS_MAX) -> list[dict]:
366
+ """Return up to *limit* records written by this process, newest first.
367
+
368
+ Only the last ``RECENT_RECORDS_MAX`` records are kept, in memory, with
369
+ every backend; records written before the process started are not
370
+ read back. Use :meth:`verify_chain` for the persisted chain.
371
+ """
372
+ if limit <= 0:
373
+ return []
374
+ with self._write_lock:
375
+ records = list(self._recent)
376
+ return records[::-1][:limit]
377
+
357
378
  def _store_to_s3(self, record: dict):
358
379
  """Persist a forensic record using the configured backend."""
359
380
  ts = datetime.now(UTC)
@@ -57,6 +57,9 @@ _PRESIDIO_TO_ADMINA: dict[str, str] = {
57
57
  }
58
58
 
59
59
  # Languages Admina configures for Presidio, each mapped to its spaCy model.
60
+ # The Presidio entity types requested from the analyzer.
61
+ _ENTITIES: list[str] = list(_PRESIDIO_TO_ADMINA)
62
+
60
63
  _LANG_MODELS: dict[str, str] = {
61
64
  "en": "en_core_web_sm",
62
65
  "it": "it_core_news_sm",
@@ -111,9 +114,11 @@ class PresidioPIIEngine:
111
114
 
112
115
  # Union of per-language analyses: a recognizer registered for only one
113
116
  # language is missed by the other pass, so both are run and merged.
117
+ # Only the mapped entity types are requested: the other recognizers
118
+ # (URL among them, slow on text with many dots) would be discarded.
114
119
  raw = []
115
120
  for lang in self.languages:
116
- raw.extend(self._analyzer.analyze(text=text, language=lang))
121
+ raw.extend(self._analyzer.analyze(text=text, language=lang, entities=_ENTITIES))
117
122
 
118
123
  spans: list[tuple[int, int, str, str]] = []
119
124
  for r in raw:
@@ -16,17 +16,17 @@
16
16
 
17
17
  Simple header-based authentication using constant-time comparison.
18
18
  Supports both ``X-API-Key`` and ``Authorization: Bearer`` headers.
19
+
20
+ Cookies are never accepted here. The bundled dashboard's browser session is
21
+ handled by the proxy itself, which admits it only on the read-only dashboard
22
+ API (see ``admina.proxy.dashboard_session``).
19
23
  """
20
24
 
21
25
  from __future__ import annotations
22
26
 
23
- import base64
24
- import hashlib
25
- import hmac
26
27
  import logging
27
28
  import os
28
29
  import secrets
29
- import time
30
30
  from typing import Any
31
31
 
32
32
  from admina.plugins.base import BaseAuthProvider
@@ -94,12 +94,13 @@ class APIKeyAuthProvider(BaseAuthProvider):
94
94
  if path in self._exempt_paths:
95
95
  return {"user_id": "anonymous", "roles": ["public"], "metadata": {}}
96
96
 
97
- # Accept a raw API key from headers/Bearer, or a signed session cookie.
97
+ # Accept only the raw API key from X-API-Key or Authorization: Bearer.
98
98
  provided = self._extract_key(request)
99
- if provided and secrets.compare_digest(provided, self._api_key):
99
+ # Compare bytes: compare_digest raises TypeError on non-ASCII str.
100
+ if provided and secrets.compare_digest(
101
+ provided.encode("utf-8"), self._api_key.encode("utf-8")
102
+ ):
100
103
  return {"user_id": "api_key_user", "roles": ["authenticated"], "metadata": {}}
101
- if self._verify_session_cookie(self._extract_session_cookie(request)):
102
- return {"user_id": "dashboard_session", "roles": ["authenticated"], "metadata": {}}
103
104
  raise PermissionError("Invalid or missing API key")
104
105
 
105
106
  async def authorize(
@@ -128,12 +129,7 @@ class APIKeyAuthProvider(BaseAuthProvider):
128
129
 
129
130
  @staticmethod
130
131
  def _extract_key(request: Any) -> str:
131
- """Extract the raw API key from request headers (X-API-Key or Bearer).
132
-
133
- The admina_session cookie is NOT extracted here — it carries a signed
134
- token, not the raw key, and is handled separately by
135
- ``_extract_session_cookie`` + ``_verify_session_cookie``.
136
- """
132
+ """Extract the raw API key from request headers (X-API-Key or Bearer)."""
137
133
  if isinstance(request, dict):
138
134
  headers = request.get("headers", {})
139
135
  else:
@@ -145,36 +141,3 @@ class APIKeyAuthProvider(BaseAuthProvider):
145
141
  if auth.startswith("Bearer "):
146
142
  key = auth.removeprefix("Bearer ").strip()
147
143
  return key
148
-
149
- @staticmethod
150
- def _extract_session_cookie(request: Any) -> str:
151
- """Extract the admina_session cookie value from a request."""
152
- if isinstance(request, dict):
153
- cookies = request.get("cookies", {})
154
- else:
155
- cookies = dict(getattr(request, "cookies", {}))
156
- return cookies.get("admina_session", "")
157
-
158
- def _verify_session_cookie(self, token: str) -> bool:
159
- """Verify a signed admina_session cookie against the configured key.
160
-
161
- Mirrors the proxy's dashboard token format (HMAC-SHA256 over the
162
- expiry). Replicated locally so this plugin does not depend on the
163
- proxy package.
164
- """
165
- if not self._api_key or not token:
166
- return False
167
- try:
168
- raw = base64.urlsafe_b64decode(token.encode("ascii")).decode("utf-8")
169
- payload, sig = raw.rsplit(".", 1)
170
- except (ValueError, UnicodeDecodeError):
171
- return False
172
- expected = hmac.new(
173
- self._api_key.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
174
- ).hexdigest()
175
- if not secrets.compare_digest(sig, expected):
176
- return False
177
- try:
178
- return int(time.time()) < int(payload)
179
- except ValueError:
180
- return False