admina-framework 0.11.1__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {admina_framework-0.11.1/admina_framework.egg-info → admina_framework-0.12.1}/PKG-INFO +8 -4
  2. {admina_framework-0.11.1 → admina_framework-0.12.1}/README.md +4 -0
  3. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/__init__.py +1 -1
  4. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/main.py +167 -0
  5. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/config.py +34 -3
  6. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/index.html +145 -4
  7. admina_framework-0.12.1/admina/domains/agent_security/coordination.py +822 -0
  8. admina_framework-0.12.1/admina/domains/agent_security/egress.py +574 -0
  9. admina_framework-0.12.1/admina/domains/agent_security/fingerprint.py +211 -0
  10. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/governance.py +43 -0
  11. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/engines/__init__.py +35 -1
  12. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/auth/apikey.py +10 -47
  13. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/dashboard.py +33 -12
  14. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/gateway.py +5 -1
  15. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/integration.py +6 -0
  16. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/config.py +16 -4
  17. admina_framework-0.12.1/admina/proxy/dashboard_session.py +133 -0
  18. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/main.py +387 -85
  19. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/state.py +10 -0
  20. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/SHA256SUMS +1 -0
  21. admina_framework-0.12.1/admina/redteam/corpora/coordination.jsonl +7 -0
  22. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_model.py +29 -0
  23. {admina_framework-0.11.1 → admina_framework-0.12.1/admina_framework.egg-info}/PKG-INFO +8 -4
  24. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/SOURCES.txt +17 -0
  25. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/requires.txt +3 -3
  26. {admina_framework-0.11.1 → admina_framework-0.12.1}/pyproject.toml +5 -5
  27. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_benchmark_14us.py +38 -0
  28. admina_framework-0.12.1/tests/test_coordination.py +1240 -0
  29. admina_framework-0.12.1/tests/test_coordination_cli.py +117 -0
  30. admina_framework-0.12.1/tests/test_coordination_corpus.py +147 -0
  31. admina_framework-0.12.1/tests/test_coordination_wiring.py +889 -0
  32. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_core_config.py +102 -0
  33. admina_framework-0.12.1/tests/test_egress_analyze.py +401 -0
  34. admina_framework-0.12.1/tests/test_egress_cli.py +257 -0
  35. admina_framework-0.12.1/tests/test_egress_pipeline.py +285 -0
  36. admina_framework-0.12.1/tests/test_egress_policy.py +137 -0
  37. admina_framework-0.12.1/tests/test_egress_surfaces.py +498 -0
  38. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_engines.py +132 -0
  39. admina_framework-0.12.1/tests/test_fingerprint.py +217 -0
  40. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_endpoints.py +1 -0
  41. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_guard_fail_mode.py +1 -0
  42. admina_framework-0.12.1/tests/test_proxy_dashboard_session.py +800 -0
  43. admina_framework-0.12.1/tests/test_proxy_metrics_endpoint.py +143 -0
  44. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_security.py +25 -72
  45. {admina_framework-0.11.1 → admina_framework-0.12.1}/LICENSE +0 -0
  46. {admina_framework-0.11.1 → admina_framework-0.12.1}/NOTICE +0 -0
  47. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/__init__.py +0 -0
  48. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/commands/__init__.py +0 -0
  49. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/admina.yaml.j2 +0 -0
  50. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/docker-compose.yml.j2 +0 -0
  51. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/env.j2 +0 -0
  52. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/main.py.j2 +0 -0
  53. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin.py.j2 +0 -0
  54. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_pyproject.toml.j2 +0 -0
  55. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_readme.md.j2 +0 -0
  56. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_test.py.j2 +0 -0
  57. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/__init__.py +0 -0
  58. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/event_bus.py +0 -0
  59. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/secrets.py +0 -0
  60. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/types.py +0 -0
  61. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/__init__.py +0 -0
  62. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/heimdall.png +0 -0
  63. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
  64. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/__init__.py +0 -0
  65. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/__init__.py +0 -0
  66. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/firewall.py +0 -0
  67. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/loop_breaker.py +0 -0
  68. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/__init__.py +0 -0
  69. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/llm_engine.py +0 -0
  70. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/rag.py +0 -0
  71. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/webui.py +0 -0
  72. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/__init__.py +0 -0
  73. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/cross_regulation.py +0 -0
  74. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/eu_ai_act.py +0 -0
  75. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/forensic.py +0 -0
  76. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/gdpr.py +0 -0
  77. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/nis2.py +0 -0
  78. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/oisg.py +0 -0
  79. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/otel.py +0 -0
  80. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/__init__.py +0 -0
  81. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/classification.py +0 -0
  82. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/pii.py +0 -0
  83. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/residency.py +0 -0
  84. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/engines/presidio.py +0 -0
  85. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/__init__.py +0 -0
  86. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/_engines.py +0 -0
  87. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/cheshirecat/__init__.py +0 -0
  88. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
  89. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/crewai/__init__.py +0 -0
  90. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/crewai/callbacks.py +0 -0
  91. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/langchain/__init__.py +0 -0
  92. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/langchain/callbacks.py +0 -0
  93. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/n8n/__init__.py +0 -0
  94. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/openclaw/__init__.py +0 -0
  95. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/__init__.py +0 -0
  96. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/base.py +0 -0
  97. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/__init__.py +0 -0
  98. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/__init__.py +0 -0
  99. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/_streaming.py +0 -0
  100. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/anthropic.py +0 -0
  101. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/bedrock.py +0 -0
  102. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/gemini.py +0 -0
  103. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/mistral.py +0 -0
  104. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/ollama.py +0 -0
  105. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/openai.py +0 -0
  106. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/vllm.py +0 -0
  107. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/__init__.py +0 -0
  108. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/log.py +0 -0
  109. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/webhook.py +0 -0
  110. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/auth/__init__.py +0 -0
  111. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/compliance/__init__.py +0 -0
  112. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/compliance/eu_ai_act.py +0 -0
  113. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/__init__.py +0 -0
  114. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/chromadb.py +0 -0
  115. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/filesystem.py +0 -0
  116. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/forensic/__init__.py +0 -0
  117. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/forensic/filesystem.py +0 -0
  118. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/guards/__init__.py +0 -0
  119. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/guards/guardrailsai_guard.py +0 -0
  120. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/pii/__init__.py +0 -0
  121. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/pii/spacy_regex.py +0 -0
  122. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/__init__.py +0 -0
  123. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/http_rest.py +0 -0
  124. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/mcp.py +0 -0
  125. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/registry.py +0 -0
  126. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/__init__.py +0 -0
  127. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/__init__.py +0 -0
  128. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/engine_bridge.py +0 -0
  129. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/governance.py +0 -0
  130. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/multi_upstream.py +0 -0
  131. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/py.typed +0 -0
  132. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/__init__.py +0 -0
  133. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/baselines/baseline.json +0 -0
  134. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/injection.jsonl +0 -0
  135. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/loop.jsonl +0 -0
  136. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/pii.jsonl +0 -0
  137. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora.py +0 -0
  138. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/detectors.py +0 -0
  139. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/gate.py +0 -0
  140. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/metrics.py +0 -0
  141. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/report.py +0 -0
  142. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/runner.py +0 -0
  143. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/__init__.py +0 -0
  144. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/_compat.py +0 -0
  145. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/compliance_kit.py +0 -0
  146. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/errors.py +0 -0
  147. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_agent.py +0 -0
  148. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_data.py +0 -0
  149. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/retry.py +0 -0
  150. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/streaming.py +0 -0
  151. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/dependency_links.txt +0 -0
  152. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/entry_points.txt +0 -0
  153. {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/top_level.txt +0 -0
  154. {admina_framework-0.11.1 → admina_framework-0.12.1}/setup.cfg +0 -0
  155. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_domains.py +0 -0
  156. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_domains_governance.py +0 -0
  157. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_error_disclosure.py +0 -0
  158. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_firewall_parity.py +0 -0
  159. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_forensic_blackbox.py +0 -0
  160. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_config.py +0 -0
  161. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_helpers.py +0 -0
  162. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_mount.py +0 -0
  163. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_governance_pipeline.py +0 -0
  164. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_guard_fail_mode.py +0 -0
  165. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_mcp_transport.py +0 -0
  166. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_presidio_engine.py +0 -0
  167. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_forensic_would_action.py +0 -0
  168. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_governance_mode_config.py +0 -0
  169. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_guard_fail_mode.py +0 -0
  170. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_mcp_response_pii.py +0 -0
  171. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_plugins.py +0 -0
  172. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_redteam_efficacy.py +0 -0
  173. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_redteam_lib.py +0 -0
  174. {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: admina-framework
3
- Version: 0.11.1
3
+ Version: 0.12.1
4
4
  Summary: Admina — governed AI development framework
5
5
  Author-email: Stefano Noferi <info@admina.org>
6
6
  Maintainer-email: Stefano Noferi <info@admina.org>
@@ -52,15 +52,15 @@ Requires-Dist: opentelemetry-sdk<2,>=1.20; extra == "telemetry"
52
52
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc<2,>=1.20; extra == "telemetry"
53
53
  Requires-Dist: opentelemetry-instrumentation-fastapi<1,>=0.40b0; extra == "telemetry"
54
54
  Provides-Extra: rust
55
- Requires-Dist: admina-core<0.12,>=0.9.3; extra == "rust"
55
+ Requires-Dist: admina-core<0.13,>=0.9.3; extra == "rust"
56
56
  Provides-Extra: full
57
57
  Requires-Dist: admina-framework[nlp,proxy,telemetry]; extra == "full"
58
58
  Provides-Extra: openai
59
- Requires-Dist: openai<3,>=1.0; extra == "openai"
59
+ Requires-Dist: openai<4,>=1.0; extra == "openai"
60
60
  Provides-Extra: ollama
61
61
  Requires-Dist: ollama<1,>=0.3; extra == "ollama"
62
62
  Provides-Extra: anthropic
63
- Requires-Dist: anthropic<1,>=0.39; extra == "anthropic"
63
+ Requires-Dist: anthropic<2,>=0.39; extra == "anthropic"
64
64
  Provides-Extra: mistral
65
65
  Requires-Dist: mistralai<3,>=1.0; extra == "mistral"
66
66
  Provides-Extra: gemini
@@ -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.11.1"
26
+ __version__ = "0.12.1"
27
27
 
28
28
  __all__ = [
29
29
  "__version__",
@@ -20,6 +20,7 @@ Entry point: ``admina = "cli.main:app"`` in pyproject.toml.
20
20
  from __future__ import annotations
21
21
 
22
22
  import hashlib
23
+ import json
23
24
  import logging
24
25
  import os
25
26
  import shutil
@@ -642,6 +643,7 @@ def _run_local(
642
643
  click.echo(f" http://{ip}:{port} ({label})")
643
644
  else:
644
645
  click.echo(f" Ready → http://{display_host}:{port}")
646
+ click.echo(" Dashboard sign-in: the API key (show it with `admina password show`)")
645
647
  if not no_browser:
646
648
  webbrowser.open(f"http://{display_host}:{port}")
647
649
  else:
@@ -1485,6 +1487,171 @@ def password_set(new_password: str) -> None:
1485
1487
  click.echo(" Restart services to apply: docker compose up --build -d\n")
1486
1488
 
1487
1489
 
1490
+ # ── admina egress commands ───────────────────────────────────
1491
+
1492
+
1493
+ @app.group()
1494
+ def egress() -> None:
1495
+ """Inspect and manage the egress policy."""
1496
+
1497
+
1498
+ @egress.command("suggest-allowlist")
1499
+ @click.option(
1500
+ "--since",
1501
+ type=click.IntRange(min=0),
1502
+ default=7,
1503
+ help="Only consider records from the last N days.",
1504
+ )
1505
+ @click.option(
1506
+ "--forensic-dir",
1507
+ default=None,
1508
+ help=(
1509
+ "Directory holding forensic records (the filesystem store's base_dir). "
1510
+ "Defaults to $FORENSIC_BASE_DIR, then .admina/forensic."
1511
+ ),
1512
+ )
1513
+ def suggest_allowlist(since: int, forensic_dir: str | None) -> None:
1514
+ """Print a candidate allowlist from destinations seen in observe mode."""
1515
+ # Same source `admina doctor` reads for the filesystem store; the flag
1516
+ # stays an explicit override.
1517
+ base = Path(forensic_dir or os.environ.get("FORENSIC_BASE_DIR") or ".admina/forensic")
1518
+ cutoff = time.time() - since * 86400
1519
+ seen: set[str] = set()
1520
+ for path in sorted(base.rglob("*.json")) if base.is_dir() else []:
1521
+ try:
1522
+ if path.stat().st_mtime < cutoff:
1523
+ continue
1524
+ record = json.loads(path.read_text())
1525
+ except (OSError, ValueError):
1526
+ continue
1527
+ # Handle both wrapped (ForensicBlackBox) and flat (test) record shapes
1528
+ if not isinstance(record, dict):
1529
+ continue
1530
+ event = record.get("event", record)
1531
+ if not isinstance(event, dict):
1532
+ continue
1533
+ check = event.get("checks") or {}
1534
+ if not isinstance(check, dict):
1535
+ continue
1536
+ egress = check.get("egress") or {}
1537
+ if not isinstance(egress, dict):
1538
+ continue
1539
+ destinations = egress.get("destinations") or []
1540
+ if not isinstance(destinations, list):
1541
+ continue
1542
+ for dest in destinations:
1543
+ if isinstance(dest, str) and dest:
1544
+ seen.add(dest)
1545
+
1546
+ if not seen:
1547
+ click.echo(f"No egress observations found in {base} for the last {since} day(s).")
1548
+ click.echo("Run with ADMINA_EGRESS_MODE=observe to collect them.")
1549
+ click.echo(
1550
+ "Observations are read back from forensic records, and FORENSIC_BACKEND "
1551
+ "defaults to 'memory' — set FORENSIC_BACKEND=filesystem and "
1552
+ "FORENSIC_BASE_DIR for anything to be written."
1553
+ )
1554
+ return
1555
+
1556
+ click.echo("# Candidate allowlist from observed traffic.")
1557
+ click.echo("# Review every entry before promoting it: observation records what")
1558
+ click.echo("# happened, not what should be allowed.")
1559
+ click.echo("domains:")
1560
+ click.echo(" agent_security:")
1561
+ click.echo(" egress:")
1562
+ click.echo(" allow:")
1563
+ for dest in sorted(seen):
1564
+ click.echo(f" - {dest}")
1565
+
1566
+
1567
+ def _quarantine_redis(url: str):
1568
+ """Open an async Redis client. Isolated so tests can substitute it."""
1569
+ import redis.asyncio as aioredis
1570
+
1571
+ return aioredis.from_url(url, decode_responses=True)
1572
+
1573
+
1574
+ @egress.group()
1575
+ def quarantine() -> None:
1576
+ """Inspect and clear quarantined destinations."""
1577
+
1578
+
1579
+ @quarantine.command("list")
1580
+ @click.option(
1581
+ "--redis-url",
1582
+ default=lambda: os.environ.get("REDIS_URL", "redis://localhost:6379/0"),
1583
+ )
1584
+ def quarantine_list(redis_url: str) -> None:
1585
+ """Show destinations currently refused for payload-bearing calls."""
1586
+ import asyncio
1587
+ import time
1588
+
1589
+ from admina.domains.agent_security.coordination import QuarantineStore
1590
+
1591
+ async def _run():
1592
+ client = _quarantine_redis(redis_url)
1593
+ try:
1594
+ store = QuarantineStore(client, ttl_seconds=1)
1595
+ return await store.current_raising(time.time())
1596
+ finally:
1597
+ await client.close()
1598
+
1599
+ try:
1600
+ live = sorted(asyncio.run(_run()))
1601
+ except Exception as exc: # noqa: BLE001
1602
+ click.echo(f"Could not read quarantine set: {exc}")
1603
+ raise SystemExit(1)
1604
+
1605
+ if not live:
1606
+ click.echo("No destination is currently quarantined.")
1607
+ return
1608
+ click.echo("Quarantined for writes (reads are unaffected):")
1609
+ for destination in live:
1610
+ click.echo(f" - {destination}")
1611
+ click.echo("")
1612
+ click.echo(
1613
+ "Write-shaped calls to these are refused only under ADMINA_EGRESS_MODE=enforce; "
1614
+ "under the default observe mode they are recorded and allowed."
1615
+ )
1616
+
1617
+
1618
+ @quarantine.command("lift")
1619
+ @click.argument("destination")
1620
+ @click.option(
1621
+ "--redis-url",
1622
+ default=lambda: os.environ.get("REDIS_URL", "redis://localhost:6379/0"),
1623
+ )
1624
+ def quarantine_lift(destination: str, redis_url: str) -> None:
1625
+ """Clear one destination's quarantine."""
1626
+ import asyncio
1627
+
1628
+ from admina.domains.agent_security.coordination import QuarantineStore
1629
+
1630
+ # Destinations are stored as the egress analyser normalised them, which
1631
+ # lower-cases every host. An operator reading WIKI.CORP off a dashboard
1632
+ # would otherwise be told it was not quarantined.
1633
+ destination = destination.strip().lower()
1634
+
1635
+ async def _run():
1636
+ client = _quarantine_redis(redis_url)
1637
+ try:
1638
+ store = QuarantineStore(client, ttl_seconds=1)
1639
+ return await store.lift_raising(destination)
1640
+ finally:
1641
+ await client.close()
1642
+
1643
+ try:
1644
+ lifted = asyncio.run(_run())
1645
+ except Exception as exc: # noqa: BLE001
1646
+ click.echo(f"Could not reach quarantine store: {exc}")
1647
+ raise SystemExit(1)
1648
+
1649
+ if lifted:
1650
+ click.echo(f"Lifted quarantine on {destination}.")
1651
+ else:
1652
+ click.echo(f"{destination} was not quarantined.")
1653
+
1654
+
1488
1655
  @app.command()
1489
1656
  @click.option(
1490
1657
  "--output",
@@ -152,6 +152,20 @@ class LoopBreakerConfig:
152
152
  max_consecutive: int = 3
153
153
 
154
154
 
155
+ @dataclass
156
+ class EgressConfig:
157
+ """Destination-based egress control settings."""
158
+
159
+ enabled: bool = True
160
+ allow: list[str] = field(default_factory=list)
161
+ read_only_tools: list[str] = field(default_factory=list)
162
+ # Parsed now, consumed by the coordination detector.
163
+ coordination_declared: list[str] = field(default_factory=list)
164
+ fanin_window_seconds: int = 3600
165
+ fanin_min_agents: int = 5
166
+ quarantine_ttl_seconds: int = 86400
167
+
168
+
155
169
  @dataclass
156
170
  class AgentSecurityConfig:
157
171
  """Agent-security domain."""
@@ -160,6 +174,7 @@ class AgentSecurityConfig:
160
174
  proxy: ProxyConfig = field(default_factory=ProxyConfig)
161
175
  firewall: FirewallConfig = field(default_factory=FirewallConfig)
162
176
  loop_breaker: LoopBreakerConfig = field(default_factory=LoopBreakerConfig)
177
+ egress: EgressConfig = field(default_factory=EgressConfig)
163
178
 
164
179
 
165
180
  @dataclass
@@ -318,9 +333,16 @@ def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
318
333
 
319
334
  # agent_security
320
335
  as_raw = domains.get("agent_security", {})
321
- px_raw = as_raw.get("proxy", {})
322
- fw_raw = as_raw.get("firewall", {})
323
- lb_raw = as_raw.get("loop_breaker", {})
336
+ # ``or {}``: a section written with nothing under it ("egress:") parses
337
+ # as None, and the next .get() would raise. For egress that exception is
338
+ # swallowed by the factory's broad except and downgraded to an empty
339
+ # allowlist, which under enforce blocks every destination with no sign of
340
+ # why; for the other three it is an outright crash.
341
+ px_raw = as_raw.get("proxy") or {}
342
+ fw_raw = as_raw.get("firewall") or {}
343
+ lb_raw = as_raw.get("loop_breaker") or {}
344
+ eg_raw = as_raw.get("egress") or {}
345
+ eg_fanin = eg_raw.get("fanin", {}) or {}
324
346
  agent_sec = AgentSecurityConfig(
325
347
  enabled=as_raw.get("enabled", True),
326
348
  proxy=ProxyConfig(
@@ -339,6 +361,15 @@ def _build_from_yaml(data: dict[str, Any]) -> AdminaConfig:
339
361
  similarity_threshold=lb_raw.get("similarity_threshold", 0.85),
340
362
  max_consecutive=lb_raw.get("max_consecutive", 3),
341
363
  ),
364
+ egress=EgressConfig(
365
+ enabled=eg_raw.get("enabled", True),
366
+ allow=list(eg_raw.get("allow") or []),
367
+ read_only_tools=list(eg_raw.get("read_only_tools") or []),
368
+ coordination_declared=list(eg_raw.get("coordination_declared") or []),
369
+ fanin_window_seconds=int(eg_fanin.get("window_seconds", 3600)),
370
+ fanin_min_agents=int(eg_fanin.get("min_agents", 5)),
371
+ quarantine_ttl_seconds=int(eg_raw.get("quarantine_ttl_seconds", 86400)),
372
+ ),
342
373
  )
343
374
 
344
375
  # compliance
@@ -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>
@@ -807,7 +856,9 @@
807
856
  * Admina Governance Dashboard — Alpine.js application.
808
857
  *
809
858
  * Reads from the Phase 3.1 API endpoints. Does NOT write.
810
- * All API calls go through the nginx reverse proxy (/api/*).
859
+ * All API calls go to /api/* on the same origin: the proxy itself in
860
+ * `admina dev` local mode (browser session after sign-in), or the nginx
861
+ * reverse proxy of the Docker stack (which adds the API key).
811
862
  */
812
863
  function dashboard() {
813
864
  return {
@@ -824,6 +875,12 @@ function dashboard() {
824
875
  modelData: {},
825
876
  oisg: {},
826
877
  showAssessmentHelp: false,
878
+ needsLogin: false,
879
+ sessionActive: false,
880
+ loginKey: '',
881
+ loginError: '',
882
+ loginBusy: false,
883
+ _started: false,
827
884
  _ws: null,
828
885
  _feedId: 0,
829
886
  _refreshTimer: null,
@@ -920,17 +977,96 @@ function dashboard() {
920
977
 
921
978
  // ── Init ───────────────────────────────────────────
922
979
  async init() {
923
- await this.refresh();
980
+ if (await this.checkSession()) this.start();
981
+ },
982
+
983
+ start() {
984
+ // Alpine calls init() both automatically and via x-init: start once.
985
+ if (this._started) return;
986
+ this._started = true;
987
+ this.refresh();
924
988
  this.connectWebSocket();
925
989
  // Auto-refresh every 10s
926
990
  this._refreshTimer = setInterval(() => this.refresh(), 10000);
927
991
  },
928
992
 
993
+ // ── Session ────────────────────────────────────────
994
+ // Serving this page never signs the browser in. The API key is exchanged
995
+ // once for a short-lived HttpOnly session cookie that only the read-only
996
+ // dashboard API accepts. Behind the Docker nginx the proxy already
997
+ // receives the key, so no sign-in is needed there.
998
+ async checkSession() {
999
+ try {
1000
+ const r = await fetch('/api/dashboard/session', { cache: 'no-store' });
1001
+ if (r.status === 401) { this.showLogin(''); return false; }
1002
+ if (r.ok) this.sessionActive = !!(await r.json()).session;
1003
+ } catch (e) { /* network error: start anyway and show Disconnected */ }
1004
+ return true;
1005
+ },
1006
+
1007
+ async login() {
1008
+ if (!this.loginKey) return;
1009
+ this.loginBusy = true;
1010
+ this.loginError = '';
1011
+ try {
1012
+ const r = await fetch('/api/dashboard/session', {
1013
+ method: 'POST',
1014
+ headers: { 'X-API-Key': this.loginKey },
1015
+ cache: 'no-store',
1016
+ });
1017
+ if (r.ok) {
1018
+ this.loginKey = '';
1019
+ this.needsLogin = false;
1020
+ this.sessionActive = true;
1021
+ this.start();
1022
+ } else if (r.status === 401) {
1023
+ this.loginError = 'Invalid API key.';
1024
+ } else {
1025
+ let detail = '';
1026
+ try { detail = (await r.json()).detail || ''; } catch (e) { /* not JSON */ }
1027
+ this.loginError = detail || `Sign-in failed (HTTP ${r.status}).`;
1028
+ }
1029
+ } catch (e) {
1030
+ this.loginError = 'Admina is not reachable.';
1031
+ } finally {
1032
+ this.loginBusy = false;
1033
+ }
1034
+ },
1035
+
1036
+ async logout() {
1037
+ try { await fetch('/api/dashboard/session', { method: 'DELETE', cache: 'no-store' }); }
1038
+ catch (e) { /* cookie expires on its own */ }
1039
+ this.showLogin('');
1040
+ },
1041
+
1042
+ showLogin(message) {
1043
+ clearInterval(this._refreshTimer);
1044
+ this._refreshTimer = null;
1045
+ this._started = false;
1046
+ if (this._ws) {
1047
+ const ws = this._ws;
1048
+ this._ws = null;
1049
+ ws.onclose = null;
1050
+ ws.close();
1051
+ }
1052
+ this.wsConnected = false;
1053
+ this.sessionActive = false;
1054
+ this.feedEvents = [];
1055
+ this.loginError = message;
1056
+ this.needsLogin = true;
1057
+ this.$nextTick(() => this.$refs.loginKey && this.$refs.loginKey.focus());
1058
+ },
1059
+
929
1060
  // ── Fetch all data ─────────────────────────────────
930
1061
  async refresh() {
931
1062
  // allSettled so one failing endpoint never blanks the whole dashboard.
932
1063
  const okJson = async (url) => {
933
1064
  const r = await fetch(url);
1065
+ if (r.status === 401) {
1066
+ const err = new Error(`${url} -> 401`);
1067
+ err.unauthorized = true;
1068
+ throw err;
1069
+ }
934
1070
  if (!r.ok) throw new Error(`${url} -> ${r.status}`);
935
1071
  return r.json();
936
1072
  };
@@ -944,6 +1080,10 @@ function dashboard() {
944
1080
  okJson('/api/dashboard/models'),
945
1081
  okJson('/api/dashboard/oisg'),
946
1082
  ]);
1083
+ if (results.some(r => r.status === 'rejected' && r.reason && r.reason.unauthorized)) {
1084
+ this.showLogin('Your session has expired. Sign in again.');
1085
+ return;
1086
+ }
947
1087
  const [scoreRes, compRes, sovRes, statsRes, feedRes, infraRes, modelsRes, oisgRes] =
948
1088
  results.map(r => r.status === 'fulfilled' ? r.value : null);
949
1089
 
@@ -998,8 +1138,9 @@ function dashboard() {
998
1138
 
999
1139
  this._ws.onclose = () => {
1000
1140
  this.wsConnected = false;
1141
+ if (this.needsLogin) return;
1001
1142
  console.log('[Admina] WebSocket disconnected, reconnecting in 3s...');
1002
- setTimeout(() => this.connectWebSocket(), 3000);
1143
+ setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 3000);
1003
1144
  };
1004
1145
 
1005
1146
  this._ws.onerror = () => {
@@ -1007,7 +1148,7 @@ function dashboard() {
1007
1148
  };
1008
1149
  } catch (e) {
1009
1150
  this.wsConnected = false;
1010
- setTimeout(() => this.connectWebSocket(), 5000);
1151
+ setTimeout(() => { if (!this.needsLogin) this.connectWebSocket(); }, 5000);
1011
1152
  }
1012
1153
  },
1013
1154