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.
- {admina_framework-0.11.1/admina_framework.egg-info → admina_framework-0.12.1}/PKG-INFO +8 -4
- {admina_framework-0.11.1 → admina_framework-0.12.1}/README.md +4 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/__init__.py +1 -1
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/main.py +167 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/config.py +34 -3
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/index.html +145 -4
- admina_framework-0.12.1/admina/domains/agent_security/coordination.py +822 -0
- admina_framework-0.12.1/admina/domains/agent_security/egress.py +574 -0
- admina_framework-0.12.1/admina/domains/agent_security/fingerprint.py +211 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/governance.py +43 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/engines/__init__.py +35 -1
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/auth/apikey.py +10 -47
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/dashboard.py +33 -12
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/gateway.py +5 -1
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/integration.py +6 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/config.py +16 -4
- admina_framework-0.12.1/admina/proxy/dashboard_session.py +133 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/main.py +387 -85
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/state.py +10 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/SHA256SUMS +1 -0
- admina_framework-0.12.1/admina/redteam/corpora/coordination.jsonl +7 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_model.py +29 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1/admina_framework.egg-info}/PKG-INFO +8 -4
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/SOURCES.txt +17 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/requires.txt +3 -3
- {admina_framework-0.11.1 → admina_framework-0.12.1}/pyproject.toml +5 -5
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_benchmark_14us.py +38 -0
- admina_framework-0.12.1/tests/test_coordination.py +1240 -0
- admina_framework-0.12.1/tests/test_coordination_cli.py +117 -0
- admina_framework-0.12.1/tests/test_coordination_corpus.py +147 -0
- admina_framework-0.12.1/tests/test_coordination_wiring.py +889 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_core_config.py +102 -0
- admina_framework-0.12.1/tests/test_egress_analyze.py +401 -0
- admina_framework-0.12.1/tests/test_egress_cli.py +257 -0
- admina_framework-0.12.1/tests/test_egress_pipeline.py +285 -0
- admina_framework-0.12.1/tests/test_egress_policy.py +137 -0
- admina_framework-0.12.1/tests/test_egress_surfaces.py +498 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_engines.py +132 -0
- admina_framework-0.12.1/tests/test_fingerprint.py +217 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_endpoints.py +1 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_guard_fail_mode.py +1 -0
- admina_framework-0.12.1/tests/test_proxy_dashboard_session.py +800 -0
- admina_framework-0.12.1/tests/test_proxy_metrics_endpoint.py +143 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_security.py +25 -72
- {admina_framework-0.11.1 → admina_framework-0.12.1}/LICENSE +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/NOTICE +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/commands/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/admina.yaml.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/docker-compose.yml.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/env.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/main.py.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin.py.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_pyproject.toml.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_readme.md.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/cli/templates/plugin_test.py.j2 +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/event_bus.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/secrets.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/core/types.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/heimdall.png +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/dashboard/static/vendor/alpinejs.min.js +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/firewall.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/agent_security/loop_breaker.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/llm_engine.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/rag.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/ai_infra/webui.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/cross_regulation.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/eu_ai_act.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/forensic.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/gdpr.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/nis2.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/oisg.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/compliance/otel.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/classification.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/pii.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/domains/data_sovereignty/residency.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/engines/presidio.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/_engines.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/cheshirecat/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/cheshirecat/admina-plugin/admina_governance.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/crewai/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/crewai/callbacks.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/langchain/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/langchain/callbacks.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/n8n/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/integrations/openclaw/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/base.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/_streaming.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/anthropic.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/bedrock.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/gemini.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/mistral.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/ollama.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/openai.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/adapters/vllm.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/log.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/alerts/webhook.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/auth/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/compliance/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/compliance/eu_ai_act.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/chromadb.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/connectors/filesystem.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/forensic/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/forensic/filesystem.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/guards/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/guards/guardrailsai_guard.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/pii/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/pii/spacy_regex.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/http_rest.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/builtin/transports/mcp.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/plugins/registry.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/api/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/engine_bridge.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/governance.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/proxy/multi_upstream.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/py.typed +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/baselines/baseline.json +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/injection.jsonl +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/loop.jsonl +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora/pii.jsonl +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/corpora.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/detectors.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/gate.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/metrics.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/report.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/redteam/runner.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/__init__.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/_compat.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/compliance_kit.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/errors.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_agent.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/governed_data.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/retry.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina/sdk/streaming.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/dependency_links.txt +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/entry_points.txt +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/admina_framework.egg-info/top_level.txt +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/setup.cfg +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_domains.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_domains_governance.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_error_disclosure.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_firewall_parity.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_forensic_blackbox.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_config.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_helpers.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_gateway_mount.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_governance_pipeline.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_guard_fail_mode.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_mcp_transport.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_presidio_engine.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_forensic_would_action.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_governance_mode_config.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_guard_fail_mode.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_mcp_response_pii.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_proxy_plugins.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_redteam_efficacy.py +0 -0
- {admina_framework-0.11.1 → admina_framework-0.12.1}/tests/test_redteam_lib.py +0 -0
- {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.
|
|
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.
|
|
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<
|
|
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<
|
|
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):
|
|
@@ -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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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.
|
|
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
|
|