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/doctor.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
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
|
+
"""``nat doctor`` — pre-flight environment validation checks.
|
|
7
|
+
|
|
8
|
+
Run via ``nat doctor [--json] [--verbose]``.
|
|
9
|
+
|
|
10
|
+
Exit codes:
|
|
11
|
+
0 All checks passed (no FAILs)
|
|
12
|
+
1 One or more checks failed
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import importlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import List, Optional
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Result model
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
_STATUS_PASS = "pass"
|
|
30
|
+
_STATUS_FAIL = "fail"
|
|
31
|
+
_STATUS_WARN = "warn"
|
|
32
|
+
_STATUS_SKIP = "skip"
|
|
33
|
+
|
|
34
|
+
_STATUS_ICON = {
|
|
35
|
+
_STATUS_PASS: "✅",
|
|
36
|
+
_STATUS_FAIL: "❌",
|
|
37
|
+
_STATUS_WARN: "⚠️ ",
|
|
38
|
+
_STATUS_SKIP: "⏭️ ",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_STATUS_LABEL = {
|
|
42
|
+
_STATUS_PASS: "PASS",
|
|
43
|
+
_STATUS_FAIL: "FAIL",
|
|
44
|
+
_STATUS_WARN: "WARN",
|
|
45
|
+
_STATUS_SKIP: "SKIP",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CheckResult:
|
|
51
|
+
name: str
|
|
52
|
+
status: str # "pass" | "fail" | "warn" | "skip"
|
|
53
|
+
message: str
|
|
54
|
+
detail: Optional[str] = field(default=None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Individual checks
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _check_python_version() -> CheckResult:
|
|
63
|
+
"""Check Python >= 3.10."""
|
|
64
|
+
info = sys.version_info
|
|
65
|
+
major = info.major
|
|
66
|
+
minor = info.minor
|
|
67
|
+
micro = info.micro
|
|
68
|
+
version_str = f"{major}.{minor}.{micro}"
|
|
69
|
+
if (major, minor) >= (3, 10):
|
|
70
|
+
return CheckResult(
|
|
71
|
+
name="Python version",
|
|
72
|
+
status=_STATUS_PASS,
|
|
73
|
+
message=f"Python {version_str} ✓",
|
|
74
|
+
)
|
|
75
|
+
return CheckResult(
|
|
76
|
+
name="Python version",
|
|
77
|
+
status=_STATUS_FAIL,
|
|
78
|
+
message=f"Python {version_str} is below the minimum required 3.10",
|
|
79
|
+
detail=(
|
|
80
|
+
"NAT requires Python 3.10 or later. "
|
|
81
|
+
"Please upgrade your Python installation."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_pip_extras() -> CheckResult:
|
|
87
|
+
"""Check that critical optional dependencies are importable."""
|
|
88
|
+
required = {
|
|
89
|
+
"yaml": "pyyaml",
|
|
90
|
+
"alembic": "alembic",
|
|
91
|
+
"httpx": "httpx",
|
|
92
|
+
"uvicorn": "uvicorn",
|
|
93
|
+
}
|
|
94
|
+
missing: List[str] = []
|
|
95
|
+
for module_name, package_name in required.items():
|
|
96
|
+
try:
|
|
97
|
+
importlib.import_module(module_name)
|
|
98
|
+
except ImportError:
|
|
99
|
+
missing.append(package_name)
|
|
100
|
+
|
|
101
|
+
if not missing:
|
|
102
|
+
return CheckResult(
|
|
103
|
+
name="Required pip extras",
|
|
104
|
+
status=_STATUS_PASS,
|
|
105
|
+
message="All critical dependencies are installed.",
|
|
106
|
+
)
|
|
107
|
+
joined = ", ".join(missing)
|
|
108
|
+
return CheckResult(
|
|
109
|
+
name="Required pip extras",
|
|
110
|
+
status=_STATUS_WARN,
|
|
111
|
+
message=f"Missing optional dependencies: {joined}",
|
|
112
|
+
detail=(
|
|
113
|
+
f"Install missing packages with: pip install {' '.join(missing)}\n"
|
|
114
|
+
"Or reinstall NAT with all extras: pip install 'nat-engine[all]'"
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _check_env_vars(natrc: dict) -> CheckResult:
|
|
120
|
+
"""Check critical environment variables based on the configured mode."""
|
|
121
|
+
mode = natrc.get("mode", "") if natrc else ""
|
|
122
|
+
|
|
123
|
+
if mode == "cli":
|
|
124
|
+
return CheckResult(
|
|
125
|
+
name="Critical environment variables",
|
|
126
|
+
status=_STATUS_SKIP,
|
|
127
|
+
message="Mode is 'cli' — no server-side env vars required.",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
missing: List[str] = []
|
|
131
|
+
hints: List[str] = []
|
|
132
|
+
|
|
133
|
+
if mode in ("", "self-hosted"):
|
|
134
|
+
if not os.environ.get("DATABASE_URL"):
|
|
135
|
+
missing.append("DATABASE_URL")
|
|
136
|
+
hints.append("Set DATABASE_URL in your .env file — see .env.example for the format.")
|
|
137
|
+
|
|
138
|
+
if mode in ("", "saas"):
|
|
139
|
+
if not os.environ.get("NAT_API_KEY"):
|
|
140
|
+
missing.append("NAT_API_KEY")
|
|
141
|
+
hints.append("Set NAT_API_KEY in your .env file or export it in your shell.")
|
|
142
|
+
|
|
143
|
+
if not missing:
|
|
144
|
+
return CheckResult(
|
|
145
|
+
name="Critical environment variables",
|
|
146
|
+
status=_STATUS_PASS,
|
|
147
|
+
message="All required environment variables are set.",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
joined = ", ".join(missing)
|
|
151
|
+
return CheckResult(
|
|
152
|
+
name="Critical environment variables",
|
|
153
|
+
status=_STATUS_FAIL,
|
|
154
|
+
message=f"Missing required environment variable(s): {joined}",
|
|
155
|
+
detail="\n".join(hints),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _check_natrc(config_path: Optional[str] = None) -> CheckResult:
|
|
160
|
+
"""Attempt to load .natrc and report results."""
|
|
161
|
+
# Lazy import to avoid circular deps
|
|
162
|
+
from mannf.product.cli import _load_natrc # noqa: PLC0415
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
data = _load_natrc(config_path)
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
return CheckResult(
|
|
168
|
+
name=".natrc configuration",
|
|
169
|
+
status=_STATUS_FAIL,
|
|
170
|
+
message="Found .natrc but could not parse it.",
|
|
171
|
+
detail=f"Parse error: {exc}\nFix the YAML syntax in your .natrc file.",
|
|
172
|
+
)
|
|
173
|
+
except OSError as exc:
|
|
174
|
+
return CheckResult(
|
|
175
|
+
name=".natrc configuration",
|
|
176
|
+
status=_STATUS_FAIL,
|
|
177
|
+
message="Error reading .natrc file.",
|
|
178
|
+
detail=f"I/O error: {exc}",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if not data and data is not None:
|
|
182
|
+
# File found but empty
|
|
183
|
+
return CheckResult(
|
|
184
|
+
name=".natrc configuration",
|
|
185
|
+
status=_STATUS_WARN,
|
|
186
|
+
message=".natrc was found but is empty.",
|
|
187
|
+
detail="Add at minimum a 'mode' key. Run `nat setup` to generate a config.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if data:
|
|
191
|
+
key_count = len(data)
|
|
192
|
+
# Try to detect the path used
|
|
193
|
+
import pathlib # noqa: PLC0415
|
|
194
|
+
|
|
195
|
+
candidates = [
|
|
196
|
+
str(pathlib.Path.cwd() / ".natrc"),
|
|
197
|
+
str(pathlib.Path.home() / ".natrc"),
|
|
198
|
+
]
|
|
199
|
+
found_path = next((p for p in candidates if os.path.isfile(p)), "<config>")
|
|
200
|
+
return CheckResult(
|
|
201
|
+
name=".natrc configuration",
|
|
202
|
+
status=_STATUS_PASS,
|
|
203
|
+
message=f"Loaded {found_path} ({key_count} key{'s' if key_count != 1 else ''}).",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return CheckResult(
|
|
207
|
+
name=".natrc configuration",
|
|
208
|
+
status=_STATUS_WARN,
|
|
209
|
+
message=".natrc not found in cwd or home directory.",
|
|
210
|
+
detail="Run `nat setup` to create a .natrc configuration file.",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _check_database() -> CheckResult:
|
|
215
|
+
"""Try a simple SELECT 1 against DATABASE_URL."""
|
|
216
|
+
db_url = os.environ.get("DATABASE_URL")
|
|
217
|
+
if not db_url:
|
|
218
|
+
return CheckResult(
|
|
219
|
+
name="Database connectivity",
|
|
220
|
+
status=_STATUS_SKIP,
|
|
221
|
+
message="DATABASE_URL is not set — skipping DB connectivity check.",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
import sqlalchemy # noqa: PLC0415
|
|
226
|
+
|
|
227
|
+
engine = sqlalchemy.create_engine(db_url, pool_pre_ping=True)
|
|
228
|
+
with engine.connect() as conn:
|
|
229
|
+
conn.execute(sqlalchemy.text("SELECT 1"))
|
|
230
|
+
engine.dispose()
|
|
231
|
+
except ImportError:
|
|
232
|
+
return CheckResult(
|
|
233
|
+
name="Database connectivity",
|
|
234
|
+
status=_STATUS_SKIP,
|
|
235
|
+
message="SQLAlchemy is not installed — skipping DB connectivity check.",
|
|
236
|
+
detail="Install with: pip install sqlalchemy",
|
|
237
|
+
)
|
|
238
|
+
except Exception as exc: # noqa: BLE001
|
|
239
|
+
return CheckResult(
|
|
240
|
+
name="Database connectivity",
|
|
241
|
+
status=_STATUS_FAIL,
|
|
242
|
+
message="Could not connect to the database.",
|
|
243
|
+
detail=(
|
|
244
|
+
f"Error: {exc}\n"
|
|
245
|
+
"Check that DATABASE_URL is correct and the database server is running."
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return CheckResult(
|
|
250
|
+
name="Database connectivity",
|
|
251
|
+
status=_STATUS_PASS,
|
|
252
|
+
message="Successfully connected to the database.",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _check_alembic_migrations() -> CheckResult:
|
|
257
|
+
"""Compare Alembic current head vs DB revision."""
|
|
258
|
+
db_url = os.environ.get("DATABASE_URL")
|
|
259
|
+
if not db_url:
|
|
260
|
+
return CheckResult(
|
|
261
|
+
name="Alembic migration state",
|
|
262
|
+
status=_STATUS_SKIP,
|
|
263
|
+
message="DATABASE_URL is not set — skipping migration check.",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
from alembic import command as alembic_command # noqa: PLC0415
|
|
268
|
+
from alembic.config import Config as AlembicConfig # noqa: PLC0415
|
|
269
|
+
from alembic.runtime.migration import MigrationContext # noqa: PLC0415
|
|
270
|
+
from alembic.script import ScriptDirectory # noqa: PLC0415
|
|
271
|
+
import sqlalchemy # noqa: PLC0415
|
|
272
|
+
except ImportError:
|
|
273
|
+
return CheckResult(
|
|
274
|
+
name="Alembic migration state",
|
|
275
|
+
status=_STATUS_SKIP,
|
|
276
|
+
message="Alembic is not installed — skipping migration state check.",
|
|
277
|
+
detail="Install with: pip install alembic",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Try to find alembic.ini
|
|
281
|
+
import pathlib # noqa: PLC0415
|
|
282
|
+
|
|
283
|
+
alembic_ini_candidates = [
|
|
284
|
+
pathlib.Path.cwd() / "alembic.ini",
|
|
285
|
+
pathlib.Path(__file__).parent.parent.parent.parent / "alembic.ini",
|
|
286
|
+
]
|
|
287
|
+
alembic_ini = next((str(p) for p in alembic_ini_candidates if p.exists()), None)
|
|
288
|
+
if not alembic_ini:
|
|
289
|
+
return CheckResult(
|
|
290
|
+
name="Alembic migration state",
|
|
291
|
+
status=_STATUS_SKIP,
|
|
292
|
+
message="alembic.ini not found — skipping migration state check.",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
cfg = AlembicConfig(alembic_ini)
|
|
297
|
+
cfg.set_main_option("sqlalchemy.url", db_url)
|
|
298
|
+
script = ScriptDirectory.from_config(cfg)
|
|
299
|
+
heads = set(script.get_heads())
|
|
300
|
+
|
|
301
|
+
engine = sqlalchemy.create_engine(db_url)
|
|
302
|
+
with engine.connect() as conn:
|
|
303
|
+
context = MigrationContext.configure(conn)
|
|
304
|
+
current_revs = set(context.get_current_heads())
|
|
305
|
+
engine.dispose()
|
|
306
|
+
except Exception as exc: # noqa: BLE001
|
|
307
|
+
return CheckResult(
|
|
308
|
+
name="Alembic migration state",
|
|
309
|
+
status=_STATUS_WARN,
|
|
310
|
+
message="Could not determine migration state.",
|
|
311
|
+
detail=f"Error: {exc}",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if current_revs == heads:
|
|
315
|
+
return CheckResult(
|
|
316
|
+
name="Alembic migration state",
|
|
317
|
+
status=_STATUS_PASS,
|
|
318
|
+
message="Database is up-to-date with the latest migrations.",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
pending = heads - current_revs
|
|
322
|
+
return CheckResult(
|
|
323
|
+
name="Alembic migration state",
|
|
324
|
+
status=_STATUS_WARN,
|
|
325
|
+
message=f"Database has {len(pending)} pending migration(s).",
|
|
326
|
+
detail=(
|
|
327
|
+
f"Pending revision(s): {', '.join(sorted(pending))}\n"
|
|
328
|
+
"Run `alembic upgrade head` or `nat upgrade --migrate` to apply them."
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _check_llm_keys() -> CheckResult:
|
|
334
|
+
"""Check for at least one LLM provider API key."""
|
|
335
|
+
openai = os.environ.get("OPENAI_API_KEY")
|
|
336
|
+
anthropic = os.environ.get("ANTHROPIC_API_KEY")
|
|
337
|
+
|
|
338
|
+
if openai or anthropic:
|
|
339
|
+
providers: List[str] = []
|
|
340
|
+
if openai:
|
|
341
|
+
providers.append("OpenAI")
|
|
342
|
+
if anthropic:
|
|
343
|
+
providers.append("Anthropic")
|
|
344
|
+
return CheckResult(
|
|
345
|
+
name="LLM provider keys",
|
|
346
|
+
status=_STATUS_PASS,
|
|
347
|
+
message=f"LLM key(s) found: {', '.join(providers)}.",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return CheckResult(
|
|
351
|
+
name="LLM provider keys",
|
|
352
|
+
status=_STATUS_SKIP,
|
|
353
|
+
message="No LLM provider keys set (OPENAI_API_KEY, ANTHROPIC_API_KEY). LLM features will be disabled.",
|
|
354
|
+
detail=(
|
|
355
|
+
"To enable LLM-powered features set one of:\n"
|
|
356
|
+
" export OPENAI_API_KEY=sk-...\n"
|
|
357
|
+
" export ANTHROPIC_API_KEY=sk-ant-..."
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _check_exporter(natrc: dict) -> CheckResult:
|
|
363
|
+
"""If an exporter is configured in .natrc, test its connection."""
|
|
364
|
+
if not natrc:
|
|
365
|
+
return CheckResult(
|
|
366
|
+
name="Exporter connectivity",
|
|
367
|
+
status=_STATUS_SKIP,
|
|
368
|
+
message="No .natrc loaded — skipping exporter connectivity check.",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
export_config = natrc.get("export")
|
|
372
|
+
if not export_config:
|
|
373
|
+
return CheckResult(
|
|
374
|
+
name="Exporter connectivity",
|
|
375
|
+
status=_STATUS_SKIP,
|
|
376
|
+
message="No 'export' key in .natrc — skipping exporter connectivity check.",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
exporter_name = export_config.get("type") if isinstance(export_config, dict) else None
|
|
380
|
+
if not exporter_name:
|
|
381
|
+
return CheckResult(
|
|
382
|
+
name="Exporter connectivity",
|
|
383
|
+
status=_STATUS_SKIP,
|
|
384
|
+
message="'export.type' not specified in .natrc — skipping exporter connectivity check.",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
from mannf.product.exporters import loader as exporter_loader # noqa: PLC0415
|
|
389
|
+
except ImportError as exc:
|
|
390
|
+
return CheckResult(
|
|
391
|
+
name="Exporter connectivity",
|
|
392
|
+
status=_STATUS_SKIP,
|
|
393
|
+
message="Exporter loader not available.",
|
|
394
|
+
detail=f"Import error: {exc}",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
all_exporters = exporter_loader.load_all_exporters()
|
|
398
|
+
exporter = exporter_loader.get_exporter_by_name(exporter_name, all_exporters)
|
|
399
|
+
if exporter is None:
|
|
400
|
+
return CheckResult(
|
|
401
|
+
name="Exporter connectivity",
|
|
402
|
+
status=_STATUS_FAIL,
|
|
403
|
+
message=f"Exporter '{exporter_name}' not found.",
|
|
404
|
+
detail=(
|
|
405
|
+
f"Check that 'export.type' in .natrc is a valid exporter name. "
|
|
406
|
+
f"Available exporters: {', '.join(e.name for e in all_exporters) or 'none'}."
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
ok, msg = exporter.test_connection(export_config)
|
|
412
|
+
except Exception as exc: # noqa: BLE001
|
|
413
|
+
return CheckResult(
|
|
414
|
+
name="Exporter connectivity",
|
|
415
|
+
status=_STATUS_FAIL,
|
|
416
|
+
message=f"Exporter '{exporter_name}' raised an error during test_connection().",
|
|
417
|
+
detail=f"Error: {exc}",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if ok:
|
|
421
|
+
return CheckResult(
|
|
422
|
+
name="Exporter connectivity",
|
|
423
|
+
status=_STATUS_PASS,
|
|
424
|
+
message=f"Exporter '{exporter_name}' connection test passed.",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return CheckResult(
|
|
428
|
+
name="Exporter connectivity",
|
|
429
|
+
status=_STATUS_FAIL,
|
|
430
|
+
message=f"Exporter '{exporter_name}' connection test failed: {msg}",
|
|
431
|
+
detail=(
|
|
432
|
+
f"Check your '{exporter_name}' credentials in .natrc under the 'export' key."
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
# Main entry point
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def run_doctor(args: argparse.Namespace) -> int:
|
|
443
|
+
"""Run all pre-flight checks and print results.
|
|
444
|
+
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
args:
|
|
448
|
+
Parsed CLI arguments. Reads ``json_output`` (bool) and ``verbose`` (bool).
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
int
|
|
453
|
+
0 if no checks failed, 1 if any check failed.
|
|
454
|
+
"""
|
|
455
|
+
json_output: bool = getattr(args, "json_output", False)
|
|
456
|
+
verbose: bool = getattr(args, "verbose", False)
|
|
457
|
+
config_path: Optional[str] = getattr(args, "config", None)
|
|
458
|
+
|
|
459
|
+
# Load .natrc first so we can pass it to checks that need it.
|
|
460
|
+
try:
|
|
461
|
+
from mannf.product.cli import _load_natrc # noqa: PLC0415
|
|
462
|
+
natrc = _load_natrc(config_path)
|
|
463
|
+
except Exception: # noqa: BLE001
|
|
464
|
+
natrc = {}
|
|
465
|
+
|
|
466
|
+
results: List[CheckResult] = [
|
|
467
|
+
_check_python_version(),
|
|
468
|
+
_check_pip_extras(),
|
|
469
|
+
_check_env_vars(natrc),
|
|
470
|
+
_check_natrc(config_path),
|
|
471
|
+
_check_database(),
|
|
472
|
+
_check_alembic_migrations(),
|
|
473
|
+
_check_llm_keys(),
|
|
474
|
+
_check_exporter(natrc),
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
if json_output:
|
|
478
|
+
output = [
|
|
479
|
+
{
|
|
480
|
+
"name": r.name,
|
|
481
|
+
"status": r.status,
|
|
482
|
+
"message": r.message,
|
|
483
|
+
**({"detail": r.detail} if r.detail is not None else {}),
|
|
484
|
+
}
|
|
485
|
+
for r in results
|
|
486
|
+
]
|
|
487
|
+
print(json.dumps(output, indent=2))
|
|
488
|
+
else:
|
|
489
|
+
print("🩺 NAT Doctor — Pre-flight Checks")
|
|
490
|
+
print()
|
|
491
|
+
for result in results:
|
|
492
|
+
icon = _STATUS_ICON[result.status]
|
|
493
|
+
label = _STATUS_LABEL[result.status]
|
|
494
|
+
print(f" {icon} [{label}] {result.name}: {result.message}")
|
|
495
|
+
if verbose and result.detail:
|
|
496
|
+
for line in result.detail.splitlines():
|
|
497
|
+
print(f" {line}")
|
|
498
|
+
print()
|
|
499
|
+
|
|
500
|
+
n_pass = sum(1 for r in results if r.status == _STATUS_PASS)
|
|
501
|
+
n_fail = sum(1 for r in results if r.status == _STATUS_FAIL)
|
|
502
|
+
n_warn = sum(1 for r in results if r.status == _STATUS_WARN)
|
|
503
|
+
n_skip = sum(1 for r in results if r.status == _STATUS_SKIP)
|
|
504
|
+
print(
|
|
505
|
+
f" {n_pass} passed, {n_fail} failed, {n_warn} warning{'s' if n_warn != 1 else ''}, {n_skip} skipped"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
any_fail = any(r.status == _STATUS_FAIL for r in results)
|
|
509
|
+
return 1 if any_fail else 0
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
"""Exporter plugins package — public API."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from mannf.product.exporters.azuredevops_exporter import AzureDevOpsExporter
|
|
11
|
+
from mannf.product.exporters.base import ExportResult, ExportSummary, ExporterPlugin, TestConnectionResult
|
|
12
|
+
from mannf.product.exporters.bugzilla_exporter import BugzillaExporter
|
|
13
|
+
from mannf.product.exporters.github_exporter import GitHubIssuesExporter
|
|
14
|
+
from mannf.product.exporters.gitlab_exporter import GitLabExporter
|
|
15
|
+
from mannf.product.exporters.jira_exporter import JiraExporter
|
|
16
|
+
from mannf.product.exporters.linear_exporter import LinearExporter
|
|
17
|
+
from mannf.product.exporters.pagerduty_exporter import PagerDutyExporter
|
|
18
|
+
from mannf.product.exporters.sentry_exporter import SentryExporter
|
|
19
|
+
from mannf.product.exporters.servicenow_exporter import ServiceNowExporter
|
|
20
|
+
from mannf.product.exporters.shortcut_exporter import ShortcutExporter
|
|
21
|
+
from mannf.product.exporters.webhook_exporter import WebhookExporter
|
|
22
|
+
from mannf.product.exporters.loader import (
|
|
23
|
+
get_exporter_by_name,
|
|
24
|
+
load_all_exporters,
|
|
25
|
+
load_exporters_from_directory,
|
|
26
|
+
load_exporters_from_entry_points,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Built-in exporters that are always available without entry-point registration.
|
|
30
|
+
BUILTIN_EXPORTERS: list[ExporterPlugin] = [
|
|
31
|
+
JiraExporter(),
|
|
32
|
+
LinearExporter(),
|
|
33
|
+
GitHubIssuesExporter(),
|
|
34
|
+
GitLabExporter(),
|
|
35
|
+
AzureDevOpsExporter(),
|
|
36
|
+
ShortcutExporter(),
|
|
37
|
+
ServiceNowExporter(),
|
|
38
|
+
PagerDutyExporter(),
|
|
39
|
+
WebhookExporter(),
|
|
40
|
+
SentryExporter(),
|
|
41
|
+
BugzillaExporter(),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"ExporterPlugin",
|
|
46
|
+
"ExportResult",
|
|
47
|
+
"ExportSummary",
|
|
48
|
+
"TestConnectionResult",
|
|
49
|
+
"JiraExporter",
|
|
50
|
+
"LinearExporter",
|
|
51
|
+
"GitHubIssuesExporter",
|
|
52
|
+
"GitLabExporter",
|
|
53
|
+
"AzureDevOpsExporter",
|
|
54
|
+
"ShortcutExporter",
|
|
55
|
+
"ServiceNowExporter",
|
|
56
|
+
"PagerDutyExporter",
|
|
57
|
+
"WebhookExporter",
|
|
58
|
+
"SentryExporter",
|
|
59
|
+
"BugzillaExporter",
|
|
60
|
+
"BUILTIN_EXPORTERS",
|
|
61
|
+
"load_all_exporters",
|
|
62
|
+
"load_exporters_from_directory",
|
|
63
|
+
"load_exporters_from_entry_points",
|
|
64
|
+
"get_exporter_by_name",
|
|
65
|
+
]
|