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,366 @@
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
+ """Worker pool orchestration for large-scale distributed NAT execution.
7
+
8
+ The :class:`WorkerPool` manages a pool of concurrent asyncio workers that
9
+ pull jobs from a shared :class:`~mannf.product.scheduling.queue.JobQueue` and
10
+ execute them. Workers are implemented as asyncio coroutines (lightweight),
11
+ enabling hundreds of parallel scan/test jobs on a single host.
12
+
13
+ For multi-host / cluster deployments, use :class:`RedisJobQueue` as the
14
+ backend — all nodes then compete for jobs via the same distributed queue.
15
+
16
+ Usage::
17
+
18
+ from mannf.core.agents.worker_pool import WorkerPool
19
+ from mannf.product.scheduling.queue import InMemoryJobQueue
20
+
21
+ queue = InMemoryJobQueue()
22
+ pool = WorkerPool(queue=queue, worker_count=4)
23
+
24
+ await pool.start()
25
+
26
+ job_id = await queue.enqueue("scan", {"scan_id": "abc", …})
27
+ # Workers will pick up the job automatically.
28
+
29
+ await pool.stop()
30
+ info = pool.info() # dict with worker states, queue stats, etc.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import asyncio
36
+ import logging
37
+ import time
38
+ import uuid
39
+ from dataclasses import dataclass, field
40
+ from datetime import datetime, timezone
41
+ from typing import Any, Callable, Coroutine, Dict, List, Optional
42
+
43
+ from mannf.product.scheduling.queue import Job, JobQueue
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Type alias for job handler coroutine functions.
48
+ JobHandler = Callable[[Job], Coroutine[Any, Any, None]]
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Worker state
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ @dataclass
56
+ class WorkerInfo:
57
+ """Snapshot of a single worker's state."""
58
+
59
+ worker_id: str
60
+ status: str = "idle" # idle | running | error | stopped
61
+ current_job_id: Optional[str] = None
62
+ jobs_completed: int = 0
63
+ jobs_failed: int = 0
64
+ last_heartbeat: float = field(default_factory=time.time)
65
+ started_at: float = field(default_factory=time.time)
66
+
67
+ def to_dict(self) -> Dict[str, Any]:
68
+ return {
69
+ "worker_id": self.worker_id,
70
+ "status": self.status,
71
+ "current_job_id": self.current_job_id,
72
+ "jobs_completed": self.jobs_completed,
73
+ "jobs_failed": self.jobs_failed,
74
+ "last_heartbeat": self.last_heartbeat,
75
+ "started_at": self.started_at,
76
+ }
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # WorkerPool
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ class WorkerPool:
85
+ """Asyncio worker pool that consumes jobs from a :class:`JobQueue`.
86
+
87
+ Parameters
88
+ ----------
89
+ queue:
90
+ The job queue to pull jobs from.
91
+ worker_count:
92
+ Number of concurrent worker coroutines to run.
93
+ poll_interval:
94
+ Seconds to sleep when the queue is empty before polling again.
95
+ max_attempts:
96
+ Maximum number of retry attempts for a failed job.
97
+ heartbeat_interval:
98
+ Seconds between worker heartbeat updates.
99
+ handlers:
100
+ Mapping of ``job_type`` → async callable. The callable receives the
101
+ :class:`~mannf.product.scheduling.queue.Job` and should raise on
102
+ failure. If a job type has no registered handler the job is nack'd
103
+ as failed immediately.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ queue: JobQueue,
109
+ worker_count: int = 4,
110
+ poll_interval: float = 1.0,
111
+ max_attempts: int = 3,
112
+ heartbeat_interval: float = 10.0,
113
+ handlers: Optional[Dict[str, JobHandler]] = None,
114
+ ) -> None:
115
+ self._queue = queue
116
+ self._worker_count = worker_count
117
+ self._poll_interval = poll_interval
118
+ self._max_attempts = max_attempts
119
+ self._heartbeat_interval = heartbeat_interval
120
+ self._handlers: Dict[str, JobHandler] = handlers or {}
121
+
122
+ self._workers: Dict[str, WorkerInfo] = {}
123
+ self._tasks: List[asyncio.Task[None]] = []
124
+ self._running = False
125
+ self._pool_id = str(uuid.uuid4())[:8]
126
+
127
+ # ------------------------------------------------------------------
128
+ # Handler registration
129
+ # ------------------------------------------------------------------
130
+
131
+ def register_handler(self, job_type: str, handler: JobHandler) -> None:
132
+ """Register an async *handler* for jobs of *job_type*."""
133
+ self._handlers[job_type] = handler
134
+ logger.debug("WorkerPool[%s]: registered handler for job_type=%r", self._pool_id, job_type)
135
+
136
+ # ------------------------------------------------------------------
137
+ # Lifecycle
138
+ # ------------------------------------------------------------------
139
+
140
+ async def start(self) -> None:
141
+ """Launch all worker coroutines. Returns immediately."""
142
+ if self._running:
143
+ logger.warning("WorkerPool[%s] already running", self._pool_id)
144
+ return
145
+
146
+ self._running = True
147
+ for i in range(self._worker_count):
148
+ worker_id = f"worker-{self._pool_id}-{i}"
149
+ info = WorkerInfo(worker_id=worker_id)
150
+ self._workers[worker_id] = info
151
+ task = asyncio.create_task(
152
+ self._worker_loop(worker_id),
153
+ name=f"nat-worker-{worker_id}",
154
+ )
155
+ self._tasks.append(task)
156
+
157
+ logger.info(
158
+ "WorkerPool[%s] started with %d workers",
159
+ self._pool_id,
160
+ self._worker_count,
161
+ )
162
+
163
+ async def stop(self, timeout: float = 30.0) -> None:
164
+ """Signal workers to stop and wait for in-flight jobs to finish.
165
+
166
+ Parameters
167
+ ----------
168
+ timeout:
169
+ Maximum seconds to wait for a clean drain. Workers still running
170
+ after the timeout are cancelled.
171
+ """
172
+ self._running = False
173
+ logger.info("WorkerPool[%s] stopping (timeout=%.1fs)…", self._pool_id, timeout)
174
+
175
+ try:
176
+ await asyncio.wait_for(
177
+ asyncio.gather(*self._tasks, return_exceptions=True),
178
+ timeout=timeout,
179
+ )
180
+ except asyncio.TimeoutError:
181
+ logger.warning(
182
+ "WorkerPool[%s] drain timeout exceeded; cancelling remaining workers",
183
+ self._pool_id,
184
+ )
185
+ for task in self._tasks:
186
+ if not task.done():
187
+ task.cancel()
188
+ await asyncio.gather(*self._tasks, return_exceptions=True)
189
+
190
+ self._tasks.clear()
191
+ for info in self._workers.values():
192
+ info.status = "stopped"
193
+ logger.info("WorkerPool[%s] stopped", self._pool_id)
194
+
195
+ @property
196
+ def is_running(self) -> bool:
197
+ """``True`` while the pool's worker tasks are active."""
198
+ return self._running and any(not t.done() for t in self._tasks)
199
+
200
+ # ------------------------------------------------------------------
201
+ # Scale up / down
202
+ # ------------------------------------------------------------------
203
+
204
+ async def scale(self, target_count: int) -> None:
205
+ """Adjust the number of running workers to *target_count*.
206
+
207
+ Workers are added or gracefully removed to reach the target.
208
+ """
209
+ current = sum(1 for t in self._tasks if not t.done())
210
+ delta = target_count - current
211
+
212
+ if delta > 0:
213
+ for _ in range(delta):
214
+ i = len(self._workers)
215
+ worker_id = f"worker-{self._pool_id}-{i}"
216
+ info = WorkerInfo(worker_id=worker_id)
217
+ self._workers[worker_id] = info
218
+ task = asyncio.create_task(
219
+ self._worker_loop(worker_id),
220
+ name=f"nat-worker-{worker_id}",
221
+ )
222
+ self._tasks.append(task)
223
+ logger.info(
224
+ "WorkerPool[%s] scaled up to %d workers", self._pool_id, target_count
225
+ )
226
+
227
+ elif delta < 0:
228
+ # Cancel the newest idle workers first
229
+ removed = 0
230
+ for task in reversed(self._tasks):
231
+ if removed >= abs(delta):
232
+ break
233
+ if not task.done():
234
+ task.cancel()
235
+ removed += 1
236
+ logger.info(
237
+ "WorkerPool[%s] scaled down to ~%d workers", self._pool_id, target_count
238
+ )
239
+
240
+ self._worker_count = target_count
241
+
242
+ # ------------------------------------------------------------------
243
+ # Info / status
244
+ # ------------------------------------------------------------------
245
+
246
+ def info(self) -> Dict[str, Any]:
247
+ """Return a serialisable snapshot of pool state."""
248
+ active = sum(1 for t in self._tasks if not t.done())
249
+ return {
250
+ "pool_id": self._pool_id,
251
+ "worker_count": self._worker_count,
252
+ "active_tasks": active,
253
+ "is_running": self.is_running,
254
+ "workers": [w.to_dict() for w in self._workers.values()],
255
+ }
256
+
257
+ async def full_status(self) -> Dict[str, Any]:
258
+ """Return pool info combined with live queue stats."""
259
+ queue_stats = await self._queue.status()
260
+ data = self.info()
261
+ data["queue"] = queue_stats.to_dict()
262
+ return data
263
+
264
+ # ------------------------------------------------------------------
265
+ # Internal worker loop
266
+ # ------------------------------------------------------------------
267
+
268
+ async def _worker_loop(self, worker_id: str) -> None:
269
+ """Main coroutine for a single worker.
270
+
271
+ Continuously dequeues jobs and dispatches them to the registered
272
+ handler. Backs off with `_poll_interval` when the queue is empty.
273
+ """
274
+ info = self._workers[worker_id]
275
+ logger.debug("Worker %s started", worker_id)
276
+
277
+ while self._running:
278
+ info.last_heartbeat = time.time()
279
+
280
+ try:
281
+ job = await self._queue.dequeue(worker_id=worker_id)
282
+ except Exception: # noqa: BLE001
283
+ logger.exception("Worker %s: error dequeuing job", worker_id)
284
+ await asyncio.sleep(self._poll_interval)
285
+ continue
286
+
287
+ if job is None:
288
+ # Queue is empty — back off
289
+ info.status = "idle"
290
+ info.current_job_id = None
291
+ await asyncio.sleep(self._poll_interval)
292
+ continue
293
+
294
+ info.status = "running"
295
+ info.current_job_id = job.job_id
296
+ logger.info(
297
+ "Worker %s: picked up job %s (type=%s, attempt=%d)",
298
+ worker_id,
299
+ job.job_id,
300
+ job.job_type,
301
+ job.attempt,
302
+ )
303
+
304
+ try:
305
+ await self._dispatch(job)
306
+ await self._queue.ack(job.job_id)
307
+ info.jobs_completed += 1
308
+ logger.info("Worker %s: job %s completed", worker_id, job.job_id)
309
+ except asyncio.CancelledError:
310
+ # Worker is being stopped — nack without retry
311
+ await self._queue.nack(
312
+ job.job_id,
313
+ error="Worker cancelled",
314
+ retry=False,
315
+ )
316
+ raise
317
+ except Exception as exc: # noqa: BLE001
318
+ info.jobs_failed += 1
319
+ error_msg = str(exc)
320
+ should_retry = job.attempt < self._max_attempts
321
+ logger.warning(
322
+ "Worker %s: job %s failed (attempt %d/%d, retry=%s): %s",
323
+ worker_id,
324
+ job.job_id,
325
+ job.attempt,
326
+ self._max_attempts,
327
+ should_retry,
328
+ error_msg,
329
+ )
330
+ await self._queue.nack(
331
+ job.job_id,
332
+ error=error_msg,
333
+ retry=should_retry,
334
+ max_attempts=self._max_attempts,
335
+ )
336
+ finally:
337
+ info.current_job_id = None
338
+ info.status = "idle"
339
+
340
+ info.status = "stopped"
341
+ logger.debug("Worker %s exited", worker_id)
342
+
343
+ async def _dispatch(self, job: Job) -> None:
344
+ """Dispatch a job to its registered handler."""
345
+ handler = self._handlers.get(job.job_type)
346
+ if handler is None:
347
+ raise ValueError(f"No handler registered for job_type={job.job_type!r}")
348
+ await handler(job)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Module-level singleton
353
+ # ---------------------------------------------------------------------------
354
+
355
+ _default_pool: Optional[WorkerPool] = None
356
+
357
+
358
+ def get_default_pool() -> Optional[WorkerPool]:
359
+ """Return the process-wide default :class:`WorkerPool`, or ``None``."""
360
+ return _default_pool
361
+
362
+
363
+ def set_default_pool(pool: Optional[WorkerPool]) -> None:
364
+ """Set the process-wide default :class:`WorkerPool`."""
365
+ global _default_pool
366
+ _default_pool = pool
@@ -0,0 +1,14 @@
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
+ """Anomaly detection sub-package for NAT."""
7
+
8
+ from mannf.core.anomaly.enhanced_detector import AnomalyAlert, AnomalyType, EnhancedAnomalyDetector
9
+
10
+ __all__ = [
11
+ "EnhancedAnomalyDetector",
12
+ "AnomalyAlert",
13
+ "AnomalyType",
14
+ ]