nat-engine 1__py3-none-any.whl
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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
mannf/product/status.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Component health-check helpers for the NAT status endpoint.
|
|
7
|
+
|
|
8
|
+
Each public ``check_*`` coroutine returns a :class:`ComponentResult` that
|
|
9
|
+
describes the health of one subsystem. :func:`run_all_checks` aggregates
|
|
10
|
+
them into a list and :func:`aggregate_status` derives the overall status.
|
|
11
|
+
|
|
12
|
+
Statuses
|
|
13
|
+
--------
|
|
14
|
+
- ``healthy`` — the component is reachable and operating normally.
|
|
15
|
+
- ``degraded`` — the component is reachable but reporting a non-critical issue.
|
|
16
|
+
- ``unhealthy`` — the component is critical and unavailable.
|
|
17
|
+
- ``disabled`` — the component is not configured (missing env var).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any, Dict, List
|
|
27
|
+
|
|
28
|
+
from mannf import __version__
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Data class
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComponentResult:
|
|
36
|
+
"""Structured result returned by each component health-check function."""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("name", "status", "latency_ms", "details")
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
name: str,
|
|
43
|
+
status: str,
|
|
44
|
+
latency_ms: float,
|
|
45
|
+
details: Dict[str, Any],
|
|
46
|
+
) -> None:
|
|
47
|
+
self.name = name
|
|
48
|
+
self.status = status
|
|
49
|
+
self.latency_ms = latency_ms
|
|
50
|
+
self.details = details
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"name": self.name,
|
|
55
|
+
"status": self.status,
|
|
56
|
+
"latency_ms": round(self.latency_ms, 3),
|
|
57
|
+
"details": self.details,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Individual component checks
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def check_database() -> ComponentResult:
|
|
67
|
+
"""Probe the PostgreSQL database with a lightweight ``SELECT 1``."""
|
|
68
|
+
from mannf.product.database import _get_database_url
|
|
69
|
+
|
|
70
|
+
if _get_database_url() is None:
|
|
71
|
+
return ComponentResult(
|
|
72
|
+
name="database",
|
|
73
|
+
status="disabled",
|
|
74
|
+
latency_ms=0.0,
|
|
75
|
+
details={"reason": "DATABASE_URL not configured"},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
t0 = time.perf_counter()
|
|
79
|
+
try:
|
|
80
|
+
from mannf.product.database import _build_engine
|
|
81
|
+
from sqlalchemy import text
|
|
82
|
+
|
|
83
|
+
engine = _build_engine()
|
|
84
|
+
if engine is None:
|
|
85
|
+
return ComponentResult(
|
|
86
|
+
name="database",
|
|
87
|
+
status="unhealthy",
|
|
88
|
+
latency_ms=0.0,
|
|
89
|
+
details={"error": "Engine could not be initialised"},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
details: Dict[str, Any] = {"engine": "postgresql"}
|
|
93
|
+
|
|
94
|
+
# Collect pool stats when available
|
|
95
|
+
try:
|
|
96
|
+
pool = engine.pool
|
|
97
|
+
details["pool_size"] = pool.size()
|
|
98
|
+
details["pool_checked_out"] = pool.checkedout()
|
|
99
|
+
except Exception: # noqa: BLE001
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
async with engine.connect() as conn:
|
|
103
|
+
await conn.execute(text("SELECT 1"))
|
|
104
|
+
|
|
105
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
106
|
+
return ComponentResult(
|
|
107
|
+
name="database",
|
|
108
|
+
status="healthy",
|
|
109
|
+
latency_ms=latency_ms,
|
|
110
|
+
details=details,
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc: # noqa: BLE001
|
|
113
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
114
|
+
return ComponentResult(
|
|
115
|
+
name="database",
|
|
116
|
+
status="unhealthy",
|
|
117
|
+
latency_ms=latency_ms,
|
|
118
|
+
details={"error": str(exc)},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def check_blob_storage() -> ComponentResult:
|
|
123
|
+
"""Check whether Azure Blob Storage is configured and reachable."""
|
|
124
|
+
conn_string = os.environ.get("NAT_BLOB_STORAGE_CONNECTION_STRING", "").strip()
|
|
125
|
+
if not conn_string:
|
|
126
|
+
return ComponentResult(
|
|
127
|
+
name="blob_storage",
|
|
128
|
+
status="disabled",
|
|
129
|
+
latency_ms=0.0,
|
|
130
|
+
details={"reason": "NAT_BLOB_STORAGE_CONNECTION_STRING not configured"},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
t0 = time.perf_counter()
|
|
134
|
+
try:
|
|
135
|
+
from azure.storage.blob import BlobServiceClient # type: ignore[import]
|
|
136
|
+
|
|
137
|
+
client = BlobServiceClient.from_connection_string(conn_string)
|
|
138
|
+
# Lightweight probe: list containers (first page only, max_results=1)
|
|
139
|
+
props = client.get_service_properties()
|
|
140
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
141
|
+
return ComponentResult(
|
|
142
|
+
name="blob_storage",
|
|
143
|
+
status="healthy",
|
|
144
|
+
latency_ms=latency_ms,
|
|
145
|
+
details={"provider": "azure_blob_storage"},
|
|
146
|
+
)
|
|
147
|
+
except ImportError:
|
|
148
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
149
|
+
return ComponentResult(
|
|
150
|
+
name="blob_storage",
|
|
151
|
+
status="degraded",
|
|
152
|
+
latency_ms=latency_ms,
|
|
153
|
+
details={
|
|
154
|
+
"reason": "azure-storage-blob SDK not installed",
|
|
155
|
+
"connection_string_set": True,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
except Exception as exc: # noqa: BLE001
|
|
159
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
160
|
+
return ComponentResult(
|
|
161
|
+
name="blob_storage",
|
|
162
|
+
status="unhealthy",
|
|
163
|
+
latency_ms=latency_ms,
|
|
164
|
+
details={"error": str(exc)},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def check_telemetry() -> ComponentResult:
|
|
169
|
+
"""Report whether Application Insights is configured (informational only)."""
|
|
170
|
+
conn_string = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING", "").strip()
|
|
171
|
+
if not conn_string:
|
|
172
|
+
return ComponentResult(
|
|
173
|
+
name="telemetry",
|
|
174
|
+
status="disabled",
|
|
175
|
+
latency_ms=0.0,
|
|
176
|
+
details={
|
|
177
|
+
"reason": "APPLICATIONINSIGHTS_CONNECTION_STRING not configured",
|
|
178
|
+
"connection_string_set": False,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
return ComponentResult(
|
|
182
|
+
name="telemetry",
|
|
183
|
+
status="healthy",
|
|
184
|
+
latency_ms=0.0,
|
|
185
|
+
details={
|
|
186
|
+
"provider": "azure_application_insights",
|
|
187
|
+
"connection_string_set": True,
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def check_scan_queue(scans: Dict[str, Any]) -> ComponentResult:
|
|
193
|
+
"""Report scan queue depth, running count, capacity, and max queue depth."""
|
|
194
|
+
t0 = time.perf_counter()
|
|
195
|
+
try:
|
|
196
|
+
# Import ScanStatus here to avoid circular imports at module level
|
|
197
|
+
from mannf.product.server import ScanStatus
|
|
198
|
+
|
|
199
|
+
running = sum(1 for s in scans.values() if s.get("status") == ScanStatus.RUNNING)
|
|
200
|
+
queued = sum(1 for s in scans.values() if s.get("status") == ScanStatus.PENDING)
|
|
201
|
+
capacity = int(os.environ.get("NAT_MAX_CONCURRENT_SCANS", "4"))
|
|
202
|
+
max_queue_depth = int(os.environ.get("NAT_MAX_QUEUE_DEPTH", "20"))
|
|
203
|
+
|
|
204
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
205
|
+
return ComponentResult(
|
|
206
|
+
name="scan_queue",
|
|
207
|
+
status="healthy",
|
|
208
|
+
latency_ms=latency_ms,
|
|
209
|
+
details={
|
|
210
|
+
"running": running,
|
|
211
|
+
"queued": queued,
|
|
212
|
+
"capacity": capacity,
|
|
213
|
+
"max_queue_depth": max_queue_depth,
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
except Exception as exc: # noqa: BLE001
|
|
217
|
+
latency_ms = (time.perf_counter() - t0) * 1000
|
|
218
|
+
return ComponentResult(
|
|
219
|
+
name="scan_queue",
|
|
220
|
+
status="degraded",
|
|
221
|
+
latency_ms=latency_ms,
|
|
222
|
+
details={"error": str(exc)},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def check_server_info(server_start_time: float) -> ComponentResult:
|
|
227
|
+
"""Return static server metadata (synchronous — no I/O needed)."""
|
|
228
|
+
uptime_seconds = time.monotonic() - server_start_time
|
|
229
|
+
workers = int(os.environ.get("NAT_WORKERS", "1"))
|
|
230
|
+
host = os.environ.get("NAT_HOST", "0.0.0.0")
|
|
231
|
+
port = int(os.environ.get("NAT_PORT", "8080"))
|
|
232
|
+
return ComponentResult(
|
|
233
|
+
name="server",
|
|
234
|
+
status="healthy",
|
|
235
|
+
latency_ms=0.0,
|
|
236
|
+
details={
|
|
237
|
+
"version": __version__,
|
|
238
|
+
"python_version": sys.version,
|
|
239
|
+
"platform": platform.platform(),
|
|
240
|
+
"uptime_seconds": round(uptime_seconds, 1),
|
|
241
|
+
"workers": workers,
|
|
242
|
+
"host": host,
|
|
243
|
+
"port": port,
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def check_environment() -> ComponentResult:
|
|
249
|
+
"""Return environment and deployment metadata (synchronous — no I/O needed).
|
|
250
|
+
|
|
251
|
+
Exposes ``NAT_ENV``, canary status, the deployed git SHA, and the
|
|
252
|
+
deployment timestamp so that monitoring tools and the ``/api/v1/status``
|
|
253
|
+
response can clearly distinguish staging from production instances and
|
|
254
|
+
identify which revision is running.
|
|
255
|
+
"""
|
|
256
|
+
nat_env = os.environ.get("NAT_ENV", "production")
|
|
257
|
+
is_canary = os.environ.get("CANARY_REVISION", "").lower() in ("true", "1", "yes")
|
|
258
|
+
deploy_sha = os.environ.get("DEPLOY_SHA", "")
|
|
259
|
+
deploy_timestamp = os.environ.get("DEPLOY_TIMESTAMP", "")
|
|
260
|
+
canary_weight_raw = os.environ.get("CANARY_TRAFFIC_WEIGHT", "")
|
|
261
|
+
canary_weight: Any = None
|
|
262
|
+
if canary_weight_raw:
|
|
263
|
+
try:
|
|
264
|
+
canary_weight = int(canary_weight_raw)
|
|
265
|
+
except ValueError:
|
|
266
|
+
canary_weight = canary_weight_raw
|
|
267
|
+
|
|
268
|
+
details: Dict[str, Any] = {
|
|
269
|
+
"environment": nat_env,
|
|
270
|
+
"is_staging": nat_env == "staging",
|
|
271
|
+
"is_canary": is_canary,
|
|
272
|
+
}
|
|
273
|
+
if deploy_sha:
|
|
274
|
+
details["deploy_sha"] = deploy_sha
|
|
275
|
+
if deploy_timestamp:
|
|
276
|
+
details["deploy_timestamp"] = deploy_timestamp
|
|
277
|
+
if is_canary and canary_weight is not None:
|
|
278
|
+
details["canary_traffic_weight"] = canary_weight
|
|
279
|
+
|
|
280
|
+
return ComponentResult(
|
|
281
|
+
name="environment",
|
|
282
|
+
status="healthy",
|
|
283
|
+
latency_ms=0.0,
|
|
284
|
+
details=details,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# Aggregation helpers
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
_CRITICAL_COMPONENTS = {"database"}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def aggregate_status(components: List[ComponentResult]) -> str:
|
|
296
|
+
"""Derive overall status from per-component results.
|
|
297
|
+
|
|
298
|
+
Rules
|
|
299
|
+
-----
|
|
300
|
+
- ``unhealthy`` if any **critical** component is unhealthy.
|
|
301
|
+
- ``degraded`` if any component is degraded.
|
|
302
|
+
- ``healthy`` if all components are healthy or disabled.
|
|
303
|
+
"""
|
|
304
|
+
statuses = {c.name: c.status for c in components}
|
|
305
|
+
|
|
306
|
+
for name in _CRITICAL_COMPONENTS:
|
|
307
|
+
if statuses.get(name) == "unhealthy":
|
|
308
|
+
return "unhealthy"
|
|
309
|
+
|
|
310
|
+
if any(s == "degraded" for s in statuses.values()):
|
|
311
|
+
return "degraded"
|
|
312
|
+
|
|
313
|
+
if any(s == "unhealthy" for s in statuses.values()):
|
|
314
|
+
return "degraded"
|
|
315
|
+
|
|
316
|
+
return "healthy"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def check_billing() -> ComponentResult:
|
|
320
|
+
"""Report whether Stripe billing is configured and ready.
|
|
321
|
+
|
|
322
|
+
Returns ``healthy`` when both ``STRIPE_SECRET_KEY`` and
|
|
323
|
+
``STRIPE_WEBHOOK_SECRET`` are present. Returns ``degraded`` when only
|
|
324
|
+
the secret key is set (webhook not configured). Returns ``disabled``
|
|
325
|
+
when neither key is set — billing is optional for self-hosted deployments.
|
|
326
|
+
"""
|
|
327
|
+
stripe_key = os.environ.get("STRIPE_SECRET_KEY", "").strip()
|
|
328
|
+
webhook_secret = os.environ.get("STRIPE_WEBHOOK_SECRET", "").strip()
|
|
329
|
+
|
|
330
|
+
if not stripe_key:
|
|
331
|
+
return ComponentResult(
|
|
332
|
+
name="billing",
|
|
333
|
+
status="disabled",
|
|
334
|
+
latency_ms=0.0,
|
|
335
|
+
details={
|
|
336
|
+
"reason": "STRIPE_SECRET_KEY not configured",
|
|
337
|
+
"stripe_key_set": False,
|
|
338
|
+
"webhook_secret_set": bool(webhook_secret),
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if not webhook_secret:
|
|
343
|
+
return ComponentResult(
|
|
344
|
+
name="billing",
|
|
345
|
+
status="degraded",
|
|
346
|
+
latency_ms=0.0,
|
|
347
|
+
details={
|
|
348
|
+
"reason": "STRIPE_WEBHOOK_SECRET not configured — webhook verification disabled",
|
|
349
|
+
"stripe_key_set": True,
|
|
350
|
+
"webhook_secret_set": False,
|
|
351
|
+
"stripe_mode": "test" if stripe_key.startswith("sk_test_") else "live",
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return ComponentResult(
|
|
356
|
+
name="billing",
|
|
357
|
+
status="healthy",
|
|
358
|
+
latency_ms=0.0,
|
|
359
|
+
details={
|
|
360
|
+
"stripe_key_set": True,
|
|
361
|
+
"webhook_secret_set": True,
|
|
362
|
+
"stripe_mode": "test" if stripe_key.startswith("sk_test_") else "live",
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def check_demo_mode() -> ComponentResult:
|
|
368
|
+
"""Report whether NAT demo mode is active.
|
|
369
|
+
|
|
370
|
+
Demo mode is controlled by the ``NAT_DEMO_MODE`` environment variable.
|
|
371
|
+
When active, Stripe calls use test keys, scan results use synthetic data
|
|
372
|
+
generators, and rate limits are relaxed for demo walkthroughs.
|
|
373
|
+
"""
|
|
374
|
+
raw = os.environ.get("NAT_DEMO_MODE", "").lower().strip()
|
|
375
|
+
is_demo = raw in ("true", "1", "yes")
|
|
376
|
+
|
|
377
|
+
return ComponentResult(
|
|
378
|
+
name="demo_mode",
|
|
379
|
+
status="healthy",
|
|
380
|
+
latency_ms=0.0,
|
|
381
|
+
details={
|
|
382
|
+
"demo_mode_active": is_demo,
|
|
383
|
+
"nat_demo_mode": raw or "false",
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def run_all_checks(scans: Dict[str, Any], server_start_time: float) -> List[ComponentResult]:
|
|
389
|
+
"""Run all component checks and return a list of results."""
|
|
390
|
+
import asyncio
|
|
391
|
+
|
|
392
|
+
results = await asyncio.gather(
|
|
393
|
+
check_database(),
|
|
394
|
+
check_blob_storage(),
|
|
395
|
+
check_telemetry(),
|
|
396
|
+
check_scan_queue(scans),
|
|
397
|
+
return_exceptions=False,
|
|
398
|
+
)
|
|
399
|
+
results = list(results)
|
|
400
|
+
results.append(check_server_info(server_start_time))
|
|
401
|
+
results.append(check_environment())
|
|
402
|
+
results.append(check_billing())
|
|
403
|
+
results.append(check_demo_mode())
|
|
404
|
+
return results
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Scan artifact blob storage package."""
|
|
7
|
+
|
|
8
|
+
from mannf.product.storage.artifact_store import ArtifactStore
|
|
9
|
+
|
|
10
|
+
__all__ = ["ArtifactStore"]
|