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.
Files changed (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. nat_engine-1.dist-info/top_level.txt +1 -0
@@ -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
+ ]