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,262 @@
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
+ """Coordinator Agent – implements the Extended Contract Net Protocol (ECNP).
7
+
8
+ The CoordinatorAgent receives TASK_ANNOUNCEMENT messages from PlannerAgents,
9
+ collects bids from ExecutorAgents over a configurable time window, evaluates
10
+ bids incorporating learned risk estimates, and awards contracts to the winning
11
+ bidder (Thesis §4.4).
12
+
13
+ ECNP cycle (per task):
14
+ 1. Receive TASK_ANNOUNCEMENT from PlannerAgent.
15
+ 2. Re-broadcast as announcement to ExecutorAgents (on TASK_ANNOUNCEMENT bus).
16
+ 3. Collect TASK_BID messages during the bid window (``bid_timeout_s``).
17
+ 4. Select winner: highest ``bid_value × risk_modulator``.
18
+ 5. Publish CONTRACT_AWARD to the winning agent.
19
+ 6. Wait for CONTRACT_RESULT; broadcast to PlannerAgents and AnalyzerAgents.
20
+
21
+ Safeguards (Thesis Appendix A.3):
22
+ * Renegotiation threshold: a task is re-announced if no bids arrive.
23
+ * Coordination rotation: coordinators can hand off if overloaded.
24
+ * Duplicate-contract prevention via task_id tracking.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import asyncio
30
+ import logging
31
+ from collections import defaultdict
32
+ from typing import Any, Dict, List, Optional, Set
33
+
34
+ from mannf.core.agents.bdi_agent import BDIAgent
35
+ from mannf.core.messaging.bus import MessageBus
36
+ from mannf.core.messaging.messages import Message, MessageType
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class CoordinatorAgent(BDIAgent):
42
+ """Allocates testing tasks via the Extended Contract Net Protocol.
43
+
44
+ Parameters
45
+ ----------
46
+ bid_timeout_s:
47
+ How long (seconds) to wait for bids before awarding or re-announcing.
48
+ max_renegotiations:
49
+ Maximum times a task is re-announced if no bids arrive.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ agent_id: str,
55
+ bus: MessageBus,
56
+ service_names: List[str],
57
+ bid_timeout_s: float = 0.05,
58
+ max_renegotiations: int = 2,
59
+ ) -> None:
60
+ super().__init__(agent_id, bus, service_names)
61
+ self._bid_timeout_s = bid_timeout_s
62
+ self._max_renegotiations = max_renegotiations
63
+
64
+ # Pending bids: task_id → list of bid payloads
65
+ self._pending_bids: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
66
+ # Awarded tasks: task_id → winner_id (prevent duplicates)
67
+ self._awarded: Dict[str, str] = {}
68
+ # Re-announcement counters
69
+ self._renegotiation_counts: Dict[str, int] = defaultdict(int)
70
+
71
+ # Statistics
72
+ self._contracts_awarded = 0
73
+ self._contracts_completed = 0
74
+
75
+ @property
76
+ def subscribed_types(self) -> Set[MessageType]:
77
+ return {
78
+ MessageType.TASK_ANNOUNCEMENT,
79
+ MessageType.TASK_BID,
80
+ MessageType.CONTRACT_RESULT,
81
+ MessageType.BELIEF_UPDATE,
82
+ }
83
+
84
+ # ------------------------------------------------------------------
85
+ # Message handling
86
+ # ------------------------------------------------------------------
87
+
88
+ async def _handle_message(self, message: Message) -> None:
89
+ if message.type == MessageType.TASK_ANNOUNCEMENT:
90
+ await self._handle_announcement(message)
91
+ elif message.type == MessageType.TASK_BID:
92
+ self._handle_bid(message)
93
+ elif message.type == MessageType.CONTRACT_RESULT:
94
+ await self._handle_contract_result(message)
95
+ elif message.type == MessageType.BELIEF_UPDATE:
96
+ self._absorb_peer_beliefs(
97
+ message.sender_id,
98
+ message.payload.get("beliefs", {}),
99
+ )
100
+
101
+ # ------------------------------------------------------------------
102
+ # ECNP: announcement phase
103
+ # ------------------------------------------------------------------
104
+
105
+ async def _handle_announcement(self, message: Message) -> None:
106
+ """Receive task from PlannerAgent and open bid collection."""
107
+ # Ignore own re-broadcasts to prevent infinite loops
108
+ if message.sender_id == self.agent_id:
109
+ return
110
+
111
+ task = message.payload or {}
112
+ task_id = task.get("task_id")
113
+ if not task_id or task_id in self._awarded:
114
+ return
115
+ # Deduplicate: ignore if already being processed
116
+ if task_id in self._pending_bids or task_id in self._renegotiation_counts:
117
+ return
118
+
119
+ # Re-broadcast the announcement so ExecutorAgents see it
120
+ await self._publish(
121
+ Message(MessageType.TASK_ANNOUNCEMENT, self.agent_id, task)
122
+ )
123
+
124
+ # Emit ECNP "offer" telemetry event (fail-safe)
125
+ try:
126
+ from mannf.dashboard import telemetry as _tel
127
+ await _tel.broadcast_ecnp_event(
128
+ event_kind="offer",
129
+ sender_id=self.agent_id,
130
+ contract_id=task_id,
131
+ details={
132
+ "target": task.get("target", ""),
133
+ "priority": task.get("priority", 0.0),
134
+ "risk_estimate": task.get("risk_estimate", 0.0),
135
+ "planner_id": task.get("planner_id", ""),
136
+ },
137
+ )
138
+ except Exception: # noqa: BLE001
139
+ pass
140
+
141
+ # Wait for bids then award
142
+ asyncio.get_running_loop().create_task(
143
+ self._award_after_timeout(task)
144
+ )
145
+
146
+ # ------------------------------------------------------------------
147
+ # ECNP: bid collection
148
+ # ------------------------------------------------------------------
149
+
150
+ def _handle_bid(self, message: Message) -> None:
151
+ bid = message.payload or {}
152
+ task_id = bid.get("task_id")
153
+ if task_id and task_id not in self._awarded:
154
+ self._pending_bids[task_id].append(bid)
155
+ logger.debug(
156
+ "CoordinatorAgent %s: received bid %.3f from %s for task %s",
157
+ self.agent_id, bid.get("bid_value", 0.0),
158
+ bid.get("bidder_id", "?"), task_id,
159
+ )
160
+
161
+ # ------------------------------------------------------------------
162
+ # ECNP: award phase
163
+ # ------------------------------------------------------------------
164
+
165
+ async def _award_after_timeout(self, task: Dict[str, Any]) -> None:
166
+ """Wait for bid_timeout, then award to highest bidder."""
167
+ await asyncio.sleep(self._bid_timeout_s)
168
+
169
+ task_id = task.get("task_id")
170
+ if not task_id or task_id in self._awarded:
171
+ return
172
+
173
+ bids = self._pending_bids.pop(task_id, [])
174
+ if not bids:
175
+ # No bids – renegotiate if allowed
176
+ count = self._renegotiation_counts[task_id]
177
+ if count < self._max_renegotiations:
178
+ self._renegotiation_counts[task_id] = count + 1
179
+ logger.debug(
180
+ "CoordinatorAgent %s: no bids for task %s; re-announcing (%d/%d)",
181
+ self.agent_id, task_id, count + 1, self._max_renegotiations,
182
+ )
183
+ await self._publish(
184
+ Message(MessageType.TASK_ANNOUNCEMENT, self.agent_id, task)
185
+ )
186
+ asyncio.get_running_loop().create_task(
187
+ self._award_after_timeout(task)
188
+ )
189
+ return
190
+
191
+ # Select winner: highest bid_value (risk-modulated)
192
+ winner_bid = max(bids, key=lambda b: b.get("bid_value", 0.0))
193
+ winner_id = winner_bid.get("bidder_id", "")
194
+
195
+ self._awarded[task_id] = winner_id
196
+ self._contracts_awarded += 1
197
+
198
+ contract = {
199
+ **task,
200
+ "winner_id": winner_id,
201
+ "winning_bid": winner_bid.get("bid_value", 0.0),
202
+ }
203
+ await self._publish(
204
+ Message(MessageType.CONTRACT_AWARD, self.agent_id, contract)
205
+ )
206
+ logger.debug(
207
+ "CoordinatorAgent %s: awarded task %s to %s (bid=%.3f)",
208
+ self.agent_id, task_id, winner_id, winner_bid.get("bid_value", 0.0),
209
+ )
210
+
211
+ # Emit ECNP "award" telemetry event (fail-safe)
212
+ try:
213
+ from mannf.dashboard import telemetry as _tel
214
+ await _tel.broadcast_ecnp_event(
215
+ event_kind="award",
216
+ sender_id=self.agent_id,
217
+ contract_id=task_id,
218
+ details={
219
+ "winner_id": winner_id,
220
+ "winning_bid": winner_bid.get("bid_value", 0.0),
221
+ "target": task.get("target", ""),
222
+ "all_bids": [
223
+ {"bidder_id": b.get("bidder_id"), "bid_value": b.get("bid_value", 0.0)}
224
+ for b in bids
225
+ ],
226
+ },
227
+ )
228
+ except Exception: # noqa: BLE001
229
+ pass
230
+
231
+ # ------------------------------------------------------------------
232
+ # ECNP: completion
233
+ # ------------------------------------------------------------------
234
+
235
+ async def _handle_contract_result(self, message: Message) -> None:
236
+ """Process execution outcome and update own beliefs.
237
+
238
+ Planners and Analyzers already subscribe directly to CONTRACT_RESULT
239
+ from Executors, so we don't re-broadcast to avoid message storms.
240
+ """
241
+ payload = message.payload or {}
242
+ self._contracts_completed += 1
243
+
244
+ # Update own beliefs based on outcome
245
+ target = payload.get("target", "")
246
+ if target:
247
+ passed = payload.get("passed", True)
248
+ error = payload.get("error")
249
+ evidence = 0.0 if passed else (0.5 if error else 1.0)
250
+ self._update_belief_from_result(target, evidence)
251
+
252
+ # ------------------------------------------------------------------
253
+ # Diagnostics
254
+ # ------------------------------------------------------------------
255
+
256
+ @property
257
+ def contracts_awarded(self) -> int:
258
+ return self._contracts_awarded
259
+
260
+ @property
261
+ def contracts_completed(self) -> int:
262
+ return self._contracts_completed
@@ -0,0 +1,222 @@
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
+ """Executor Agent – executes test cases awarded by the CoordinatorAgent.
7
+
8
+ The ExecutorAgent receives CONTRACT_AWARD messages, executes the requested
9
+ test against the system under test, and publishes CONTRACT_RESULT messages
10
+ back to the coordinator (Thesis §4.2).
11
+
12
+ Bidding model (ECNP §4.4):
13
+ * Bid value = expected informational gain, computed as:
14
+ bid = own_fault_belief[target] × (1 − recent_execution_load)
15
+ * Higher fault belief + lower load → higher bid → more likely to win.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import random
22
+ import string
23
+ import uuid
24
+ from collections import deque
25
+ from typing import Any, Deque, Dict, List, Optional, Set
26
+
27
+ import numpy as np
28
+
29
+ from mannf.core.agents.bdi_agent import BDIAgent
30
+ from mannf.core.distributed.system_under_test import SystemUnderTest
31
+ from mannf.core.messaging.bus import MessageBus
32
+ from mannf.core.messaging.messages import Message, MessageType
33
+ from mannf.core.testing.models import TestCase, TestResult
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ _LOAD_WINDOW = 10 # sliding window for execution load estimation
38
+
39
+
40
+ class ExecutorAgent(BDIAgent):
41
+ """Executes test cases through ECNP contract bidding.
42
+
43
+ Parameters
44
+ ----------
45
+ sut:
46
+ The system under test to execute against.
47
+ max_concurrent:
48
+ Maximum simultaneous executions (backpressure signal to bids).
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ agent_id: str,
54
+ bus: MessageBus,
55
+ service_names: List[str],
56
+ sut: SystemUnderTest,
57
+ max_concurrent: int = 3,
58
+ ) -> None:
59
+ super().__init__(agent_id, bus, service_names)
60
+ self._sut = sut
61
+ self._max_concurrent = max_concurrent
62
+ self._active: int = 0
63
+ self._latency_window: Deque[float] = deque(maxlen=_LOAD_WINDOW)
64
+ self._executions = 0
65
+
66
+ @property
67
+ def subscribed_types(self) -> Set[MessageType]:
68
+ return {
69
+ MessageType.TASK_ANNOUNCEMENT, # evaluate and potentially bid
70
+ MessageType.CONTRACT_AWARD, # execute if we won
71
+ MessageType.BELIEF_UPDATE, # absorb peer beliefs
72
+ }
73
+
74
+ # ------------------------------------------------------------------
75
+ # Message handling
76
+ # ------------------------------------------------------------------
77
+
78
+ async def _handle_message(self, message: Message) -> None:
79
+ if message.type == MessageType.TASK_ANNOUNCEMENT:
80
+ await self._evaluate_and_bid(message)
81
+ elif message.type == MessageType.CONTRACT_AWARD:
82
+ if message.payload.get("winner_id") == self.agent_id:
83
+ await self._execute_contract(message.payload)
84
+ elif message.type == MessageType.BELIEF_UPDATE:
85
+ self._absorb_peer_beliefs(
86
+ message.sender_id,
87
+ message.payload.get("beliefs", {}),
88
+ )
89
+
90
+ # ------------------------------------------------------------------
91
+ # ECNP bidding
92
+ # ------------------------------------------------------------------
93
+
94
+ async def _evaluate_and_bid(self, announcement: Message) -> None:
95
+ """Compute a bid value and submit it to the CoordinatorAgent."""
96
+ task = announcement.payload or {}
97
+ target = task.get("target", "")
98
+ if not target:
99
+ return
100
+
101
+ # Bid = fault-likelihood belief × availability factor
102
+ fault_belief = self.beliefs.fault_likelihood.get(target, 0.5)
103
+ load_factor = 1.0 - (self._active / max(self._max_concurrent, 1))
104
+ bid_value = fault_belief * max(0.1, load_factor)
105
+
106
+ await self._publish(
107
+ Message(
108
+ MessageType.TASK_BID,
109
+ sender_id=self.agent_id,
110
+ payload={
111
+ "task_id": task.get("task_id"),
112
+ "target": target,
113
+ "bid_value": bid_value,
114
+ "bidder_id": self.agent_id,
115
+ },
116
+ correlation_id=announcement.id,
117
+ )
118
+ )
119
+ logger.debug(
120
+ "ExecutorAgent %s: bid %.3f for %s",
121
+ self.agent_id, bid_value, target,
122
+ )
123
+
124
+ # Emit ECNP "bid" telemetry event (fail-safe)
125
+ try:
126
+ from mannf.dashboard import telemetry as _tel
127
+ await _tel.broadcast_ecnp_event(
128
+ event_kind="bid",
129
+ sender_id=self.agent_id,
130
+ contract_id=task.get("task_id", ""),
131
+ details={
132
+ "target": target,
133
+ "bid_value": round(bid_value, 4),
134
+ "load_factor": round(max(0.1, load_factor), 4),
135
+ "fault_belief": round(fault_belief, 4),
136
+ },
137
+ )
138
+ except Exception: # noqa: BLE001
139
+ pass
140
+
141
+ # ------------------------------------------------------------------
142
+ # Contract execution
143
+ # ------------------------------------------------------------------
144
+
145
+ async def _execute_contract(self, contract: Dict[str, Any]) -> None:
146
+ """Execute the awarded test contract against the SUT."""
147
+ target = contract.get("target", "")
148
+ if not target:
149
+ return
150
+
151
+ self._active += 1
152
+ test_case = self._build_test_case(target, contract)
153
+
154
+ try:
155
+ result = await self._sut.execute(test_case)
156
+ result.agent_id = self.agent_id
157
+ self._executions += 1
158
+
159
+ # Update own beliefs based on outcome
160
+ evidence = 0.0 if result.passed else (0.5 if result.error else 1.0)
161
+ self._update_belief_from_result(target, evidence)
162
+
163
+ if result.execution_time_ms:
164
+ self._latency_window.append(result.execution_time_ms)
165
+
166
+ # Publish result back to the bus (monitor/oracle listen)
167
+ await self._publish(
168
+ Message(
169
+ MessageType.TEST_RESULT,
170
+ sender_id=self.agent_id,
171
+ payload={"result": result, "test_case": test_case},
172
+ )
173
+ )
174
+ # Notify coordinator of completion
175
+ await self._publish(
176
+ Message(
177
+ MessageType.CONTRACT_RESULT,
178
+ sender_id=self.agent_id,
179
+ payload={
180
+ "task_id": contract.get("task_id"),
181
+ "target": target,
182
+ "passed": result.passed,
183
+ "error": result.error,
184
+ "execution_time_ms": result.execution_time_ms,
185
+ "executor_id": self.agent_id,
186
+ "test_case": test_case,
187
+ "result": result,
188
+ },
189
+ correlation_id=contract.get("task_id"),
190
+ )
191
+ )
192
+ logger.debug(
193
+ "ExecutorAgent %s: executed %s → %s",
194
+ self.agent_id, target, "PASS" if result.passed else "FAIL",
195
+ )
196
+ finally:
197
+ self._active -= 1
198
+
199
+ @staticmethod
200
+ def _build_test_case(target: str, contract: Dict[str, Any]) -> TestCase:
201
+ inputs = {
202
+ "request_id": str(uuid.uuid4()),
203
+ "user_id": random.randint(1, 10_000),
204
+ "action": random.choice(["create", "read", "update", "delete"]),
205
+ "auth_token": "".join(random.choices(string.ascii_letters, k=16)),
206
+ "payload_size": random.randint(1, 1024),
207
+ }
208
+ return TestCase(
209
+ target=target,
210
+ inputs=inputs,
211
+ expected_behavior=f"Service {target!r} returns a successful response",
212
+ priority=contract.get("risk_estimate", 0.5),
213
+ metadata={"task_id": contract.get("task_id", ""), "executor": contract.get("winner_id", "")},
214
+ )
215
+
216
+ @property
217
+ def execution_count(self) -> int:
218
+ return self._executions
219
+
220
+ @property
221
+ def avg_latency_ms(self) -> float:
222
+ return float(np.mean(list(self._latency_window))) if self._latency_window else 0.0
@@ -0,0 +1,188 @@
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
+ """Monitor agent.
7
+
8
+ The :class:`MonitorAgent` collects execution metrics from
9
+ :class:`TestResult` messages and uses an anomaly-detection neural network
10
+ to flag unusual system behaviour. It publishes
11
+ :attr:`MessageType.METRICS_UPDATE` and :attr:`MessageType.ANOMALY_DETECTED`
12
+ messages that other agents can react to.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ from collections import deque
20
+ from typing import Any, Deque, Dict, List, Optional, Set, Tuple
21
+
22
+ import numpy as np
23
+
24
+ from mannf.core.agents.base import BaseAgent
25
+ from mannf.core.messaging.bus import MessageBus
26
+ from mannf.core.messaging.messages import Message, MessageType
27
+ from mannf.core.neural import NeuralNetwork
28
+ from mannf.core.testing.models import TestResult
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _METRIC_DIM = 6 # dimensionality of the metric feature vector
33
+ _HISTORY = 50 # sliding window length for anomaly detection baseline
34
+
35
+
36
+ class MonitorAgent(BaseAgent):
37
+ """Monitors test execution metrics and detects anomalies.
38
+
39
+ The agent maintains a sliding window of recent test results and trains
40
+ an autoencoder-style anomaly detector: inputs ≈ outputs when normal;
41
+ high reconstruction error signals an anomaly.
42
+ """
43
+
44
+ def __init__(self, agent_id: str, bus: MessageBus) -> None:
45
+ # Autoencoder: compress 6-D metrics to 3-D then reconstruct
46
+ network = NeuralNetwork(
47
+ layer_sizes=[_METRIC_DIM, 12, 3, 12, _METRIC_DIM],
48
+ hidden_activation="relu",
49
+ learning_rate=0.005,
50
+ )
51
+ super().__init__(agent_id, bus, network)
52
+
53
+ self._history: Deque[np.ndarray] = deque(maxlen=_HISTORY)
54
+ self._total_tests = 0
55
+ self._total_failures = 0
56
+ self._total_errors = 0
57
+ self._latency_window: Deque[float] = deque(maxlen=_HISTORY)
58
+
59
+ # Dynamic threshold for anomaly detection (updated per batch)
60
+ self._anomaly_threshold: float = 1.0
61
+
62
+ @property
63
+ def subscribed_types(self) -> Set[MessageType]:
64
+ return {MessageType.TEST_RESULT}
65
+
66
+ # ------------------------------------------------------------------
67
+ # Message handling
68
+ # ------------------------------------------------------------------
69
+
70
+ async def _handle_message(self, message: Message) -> None:
71
+ if message.type == MessageType.TEST_RESULT:
72
+ payload = message.payload
73
+ result: TestResult = (
74
+ payload.get("result") if isinstance(payload, dict) else payload
75
+ )
76
+ if isinstance(result, TestResult):
77
+ self._ingest(result)
78
+
79
+ async def _on_idle(self) -> None:
80
+ """Periodically broadcast a metrics snapshot."""
81
+ if self._total_tests > 0:
82
+ snapshot = self._metrics_snapshot()
83
+ await self._publish(
84
+ Message(MessageType.METRICS_UPDATE, self.agent_id, snapshot)
85
+ )
86
+
87
+ # ------------------------------------------------------------------
88
+ # Metric ingestion and anomaly detection
89
+ # ------------------------------------------------------------------
90
+
91
+ def _ingest(self, result: TestResult) -> None:
92
+ self._total_tests += 1
93
+ if result.error:
94
+ self._total_errors += 1
95
+ elif not result.passed:
96
+ self._total_failures += 1
97
+ self._latency_window.append(result.execution_time_ms)
98
+
99
+ vec = self._result_to_vector(result)
100
+ self._history.append(vec)
101
+
102
+ # Train autoencoder on normal data (passed tests only)
103
+ if result.passed and not result.error and self.network is not None:
104
+ self.network.train_step(vec, vec)
105
+
106
+ # Check for anomaly once we have a baseline
107
+ if len(self._history) >= 10:
108
+ self._check_anomaly(result, vec)
109
+
110
+ def _result_to_vector(self, result: TestResult) -> np.ndarray:
111
+ """Encode a TestResult into a fixed-size numeric vector."""
112
+ vec = np.zeros(_METRIC_DIM)
113
+ vec[0] = 1.0 if result.passed else 0.0
114
+ vec[1] = 1.0 if result.error else 0.0
115
+ vec[2] = min(result.execution_time_ms / 1000.0, 5.0) # cap at 5 s
116
+ vec[3] = self._failure_rate()
117
+ vec[4] = self._error_rate()
118
+ vec[5] = self._avg_latency() / 1000.0
119
+ return vec
120
+
121
+ def _check_anomaly(self, result: TestResult, vec: np.ndarray) -> None:
122
+ """Use the autoencoder reconstruction error to detect anomalies."""
123
+ assert self.network is not None
124
+ recon = self.network.predict(vec)
125
+ error = float(np.mean((vec - recon) ** 2))
126
+
127
+ # Update threshold as 2× median of recent reconstruction errors
128
+ recent_errors = [
129
+ float(np.mean((v - self.network.predict(v)) ** 2))
130
+ for v in list(self._history)[-20:]
131
+ ]
132
+ if recent_errors:
133
+ self._anomaly_threshold = max(2.0 * float(np.median(recent_errors)), 1e-6)
134
+
135
+ if error > self._anomaly_threshold:
136
+ logger.info(
137
+ "MonitorAgent %s: anomaly detected (error=%.4f > threshold=%.4f)",
138
+ self.agent_id, error, self._anomaly_threshold,
139
+ )
140
+ try:
141
+ loop = asyncio.get_running_loop()
142
+ loop.create_task(
143
+ self._publish(
144
+ Message(
145
+ MessageType.ANOMALY_DETECTED,
146
+ self.agent_id,
147
+ {
148
+ "test_case_id": result.test_case_id,
149
+ "reconstruction_error": error,
150
+ "threshold": self._anomaly_threshold,
151
+ },
152
+ )
153
+ )
154
+ )
155
+ except RuntimeError:
156
+ pass # No running event loop; skip async publish
157
+
158
+ # ------------------------------------------------------------------
159
+ # Statistics helpers
160
+ # ------------------------------------------------------------------
161
+
162
+ def _failure_rate(self) -> float:
163
+ return self._total_failures / self._total_tests if self._total_tests else 0.0
164
+
165
+ def _error_rate(self) -> float:
166
+ return self._total_errors / self._total_tests if self._total_tests else 0.0
167
+
168
+ def _avg_latency(self) -> float:
169
+ if not self._latency_window:
170
+ return 0.0
171
+ return float(np.mean(list(self._latency_window)))
172
+
173
+ def _metrics_snapshot(self) -> Dict[str, Any]:
174
+ return {
175
+ "agent_id": self.agent_id,
176
+ "total_tests": self._total_tests,
177
+ "total_failures": self._total_failures,
178
+ "total_errors": self._total_errors,
179
+ "failure_rate": round(self._failure_rate(), 4),
180
+ "error_rate": round(self._error_rate(), 4),
181
+ "avg_latency_ms": round(self._avg_latency(), 2),
182
+ "anomaly_threshold": round(self._anomaly_threshold, 6),
183
+ }
184
+
185
+ @property
186
+ def metrics(self) -> Dict[str, Any]:
187
+ return self._metrics_snapshot()
188
+