nat-engine 1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "NAT Dashboard — NeuroAgentTest",
|
|
3
|
+
"short_name": "NAT",
|
|
4
|
+
"description": "AI-powered API testing dashboard with live agent activity, risk heatmaps, and security findings.",
|
|
5
|
+
"start_url": "/dashboard",
|
|
6
|
+
"scope": "/",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"orientation": "any",
|
|
9
|
+
"theme_color": "#1f6feb",
|
|
10
|
+
"background_color": "#0d1117",
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "/pwa-icon-192.png",
|
|
14
|
+
"sizes": "192x192",
|
|
15
|
+
"type": "image/png",
|
|
16
|
+
"purpose": "any maskable"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"src": "/pwa-icon-512.png",
|
|
20
|
+
"sizes": "512x512",
|
|
21
|
+
"type": "image/png",
|
|
22
|
+
"purpose": "any maskable"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// NAT Dashboard — Service Worker
|
|
2
|
+
// Caches static assets for offline/PWA use.
|
|
3
|
+
|
|
4
|
+
const CACHE_NAME = 'nat-dashboard-v1';
|
|
5
|
+
|
|
6
|
+
// Assets to pre-cache on install
|
|
7
|
+
const PRECACHE_URLS = [
|
|
8
|
+
'/dashboard',
|
|
9
|
+
'https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
self.addEventListener('install', (event) => {
|
|
13
|
+
event.waitUntil(
|
|
14
|
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
|
|
15
|
+
);
|
|
16
|
+
// Activate immediately without waiting for existing clients to close
|
|
17
|
+
self.skipWaiting();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener('activate', (event) => {
|
|
21
|
+
// Remove any old caches from previous versions
|
|
22
|
+
event.waitUntil(
|
|
23
|
+
caches.keys().then((keys) =>
|
|
24
|
+
Promise.all(
|
|
25
|
+
keys
|
|
26
|
+
.filter((key) => key !== CACHE_NAME)
|
|
27
|
+
.map((key) => caches.delete(key))
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
self.clients.claim();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
self.addEventListener('fetch', (event) => {
|
|
35
|
+
const { request } = event;
|
|
36
|
+
const url = new URL(request.url);
|
|
37
|
+
|
|
38
|
+
// Only handle GET requests; skip cross-origin requests except Chart.js CDN
|
|
39
|
+
if (request.method !== 'GET') return;
|
|
40
|
+
|
|
41
|
+
// Network-first strategy for API / WebSocket endpoints so live data stays fresh
|
|
42
|
+
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws/')) {
|
|
43
|
+
return; // fall through to the network
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Cache-first for everything else (dashboard HTML + Chart.js CDN)
|
|
47
|
+
event.respondWith(
|
|
48
|
+
caches.match(request).then((cached) => {
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
return fetch(request).then((response) => {
|
|
51
|
+
// Only cache successful same-origin or CDN responses
|
|
52
|
+
if (
|
|
53
|
+
response.ok &&
|
|
54
|
+
(url.origin === self.location.origin ||
|
|
55
|
+
url.hostname === 'cdn.jsdelivr.net')
|
|
56
|
+
) {
|
|
57
|
+
const clone = response.clone();
|
|
58
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
});
|
|
@@ -0,0 +1,547 @@
|
|
|
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
|
+
"""Telemetry helpers for broadcasting BDI agent state via the dashboard WebSocket.
|
|
7
|
+
|
|
8
|
+
All public functions are **non-blocking** and **fail-safe** — errors in
|
|
9
|
+
telemetry are silently swallowed so they can never crash a running scan.
|
|
10
|
+
|
|
11
|
+
Supported event types:
|
|
12
|
+
|
|
13
|
+
- ``agent_state`` – full agent snapshot after each deliberation cycle
|
|
14
|
+
- ``ecnp_event`` – ECNP contract offer / bid / award lifecycle event
|
|
15
|
+
- ``belief_update`` – incremental belief revision for a single service
|
|
16
|
+
- ``team_annotation`` – user comment added to a finding
|
|
17
|
+
- ``plan_generated`` – LLM test plan generation completed
|
|
18
|
+
- ``schedule_fired`` – a scheduled scan has been triggered
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from mannf.core.agents.bdi_agent import BDIAgent
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# In-memory registry: most-recent snapshot per agent (agent_id → payload)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
_agent_registry: Dict[str, Dict[str, Any]] = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Internal helpers
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _now_iso() -> str:
|
|
45
|
+
return datetime.now(timezone.utc).isoformat()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _broadcast(payload: Dict[str, Any]) -> None:
|
|
49
|
+
"""Broadcast *payload* via the dashboard WebSocket manager."""
|
|
50
|
+
from mannf.product.dashboard.app import manager # lazy to avoid circular import
|
|
51
|
+
|
|
52
|
+
await manager.broadcast(payload)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Public telemetry functions
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def broadcast_agent_state(agent: "BDIAgent") -> None:
|
|
61
|
+
"""Broadcast an ``agent_state`` message for *agent* to all WS clients.
|
|
62
|
+
|
|
63
|
+
Also updates the in-memory agent registry used by the
|
|
64
|
+
``GET /api/v1/dashboard/agents`` REST endpoint.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
role = type(agent).__name__.replace("Agent", "")
|
|
68
|
+
intentions = [
|
|
69
|
+
{
|
|
70
|
+
"action": i.get("action"),
|
|
71
|
+
"target": i.get("target"),
|
|
72
|
+
"score": round(float(i.get("score", 0.0)), 4),
|
|
73
|
+
}
|
|
74
|
+
for i in (agent.intentions or [])[:10]
|
|
75
|
+
]
|
|
76
|
+
confidence_per_service = {
|
|
77
|
+
k: round(float(v), 4) for k, v in agent.beliefs.confidence.items()
|
|
78
|
+
}
|
|
79
|
+
beliefs = {
|
|
80
|
+
k: round(float(v), 4) for k, v in agent.beliefs.fault_likelihood.items()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
payload: Dict[str, Any] = {
|
|
84
|
+
"type": "agent_state",
|
|
85
|
+
"agent_id": agent.agent_id,
|
|
86
|
+
"agent_role": role,
|
|
87
|
+
"beliefs": beliefs,
|
|
88
|
+
"confidence": confidence_per_service,
|
|
89
|
+
"intentions": intentions,
|
|
90
|
+
"desires": dict(agent.desires),
|
|
91
|
+
"entropy": round(agent.beliefs.entropy(), 4),
|
|
92
|
+
"timestamp": _now_iso(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_agent_registry[agent.agent_id] = payload
|
|
96
|
+
await _broadcast(payload)
|
|
97
|
+
except Exception: # noqa: BLE001
|
|
98
|
+
logger.debug("Telemetry: failed to broadcast agent_state", exc_info=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def broadcast_ecnp_event(
|
|
102
|
+
event_kind: str,
|
|
103
|
+
sender_id: str,
|
|
104
|
+
contract_id: str,
|
|
105
|
+
details: Optional[Dict[str, Any]] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Broadcast an ``ecnp_event`` message to all WebSocket clients.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
event_kind:
|
|
112
|
+
One of ``"offer"``, ``"bid"``, or ``"award"``.
|
|
113
|
+
sender_id:
|
|
114
|
+
Agent that produced the ECNP message.
|
|
115
|
+
contract_id:
|
|
116
|
+
The ``task_id`` that identifies the contract.
|
|
117
|
+
details:
|
|
118
|
+
Arbitrary payload (bid cost, awarded agent, test cases, etc.).
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
payload: Dict[str, Any] = {
|
|
122
|
+
"type": "ecnp_event",
|
|
123
|
+
"event_kind": event_kind,
|
|
124
|
+
"sender_id": sender_id,
|
|
125
|
+
"contract_id": contract_id,
|
|
126
|
+
"details": details or {},
|
|
127
|
+
"timestamp": _now_iso(),
|
|
128
|
+
}
|
|
129
|
+
await _broadcast(payload)
|
|
130
|
+
except Exception: # noqa: BLE001
|
|
131
|
+
logger.debug("Telemetry: failed to broadcast ecnp_event", exc_info=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def broadcast_belief_update(
|
|
135
|
+
agent_id: str,
|
|
136
|
+
endpoint: str,
|
|
137
|
+
old_value: float,
|
|
138
|
+
new_value: float,
|
|
139
|
+
evidence: float,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Broadcast a ``belief_update`` message to all WebSocket clients.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
agent_id:
|
|
146
|
+
The agent whose belief was revised.
|
|
147
|
+
endpoint:
|
|
148
|
+
Service / endpoint whose fault likelihood changed.
|
|
149
|
+
old_value:
|
|
150
|
+
Fault likelihood before the update.
|
|
151
|
+
new_value:
|
|
152
|
+
Fault likelihood after the update.
|
|
153
|
+
evidence:
|
|
154
|
+
The evidence value that triggered the update.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
payload: Dict[str, Any] = {
|
|
158
|
+
"type": "belief_update",
|
|
159
|
+
"agent_id": agent_id,
|
|
160
|
+
"endpoint": endpoint,
|
|
161
|
+
"old_value": round(float(old_value), 4),
|
|
162
|
+
"new_value": round(float(new_value), 4),
|
|
163
|
+
"evidence": round(float(evidence), 4),
|
|
164
|
+
"timestamp": _now_iso(),
|
|
165
|
+
}
|
|
166
|
+
await _broadcast(payload)
|
|
167
|
+
except Exception: # noqa: BLE001
|
|
168
|
+
logger.debug("Telemetry: failed to broadcast belief_update", exc_info=True)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def broadcast_usage_update(
|
|
172
|
+
tenant_id: str,
|
|
173
|
+
scan_type: str,
|
|
174
|
+
used: int,
|
|
175
|
+
limit: int,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Broadcast a ``usage_update`` message to all WebSocket clients.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
tenant_id:
|
|
182
|
+
The tenant whose usage changed.
|
|
183
|
+
scan_type:
|
|
184
|
+
``"functional"`` or ``"security"``.
|
|
185
|
+
used:
|
|
186
|
+
New usage count after the latest scan.
|
|
187
|
+
limit:
|
|
188
|
+
The tenant's monthly quota for this scan type.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
percent = round(100.0 * used / limit, 2) if limit > 0 else 0.0
|
|
192
|
+
payload: Dict[str, Any] = {
|
|
193
|
+
"type": "usage_update",
|
|
194
|
+
"tenant_id": tenant_id,
|
|
195
|
+
"scan_type": scan_type,
|
|
196
|
+
"used": used,
|
|
197
|
+
"limit": limit,
|
|
198
|
+
"percent": percent,
|
|
199
|
+
"timestamp": _now_iso(),
|
|
200
|
+
}
|
|
201
|
+
await _broadcast(payload)
|
|
202
|
+
except Exception: # noqa: BLE001
|
|
203
|
+
logger.debug("Telemetry: failed to broadcast usage_update", exc_info=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def broadcast_usage_alert(
|
|
207
|
+
tenant_id: str,
|
|
208
|
+
scan_type: str,
|
|
209
|
+
used: int,
|
|
210
|
+
limit: int,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Broadcast a ``usage_alert`` message when a tenant approaches their quota.
|
|
213
|
+
|
|
214
|
+
Only sent when usage is at or above 80% of the monthly limit. This is a
|
|
215
|
+
distinct event type from ``usage_update`` so the dashboard can surface a
|
|
216
|
+
visible upgrade nudge.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
tenant_id:
|
|
221
|
+
The tenant whose usage is near the limit.
|
|
222
|
+
scan_type:
|
|
223
|
+
``"functional"`` or ``"security"``.
|
|
224
|
+
used:
|
|
225
|
+
Current usage count for the month.
|
|
226
|
+
limit:
|
|
227
|
+
The tenant's monthly quota for this scan type.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
percent = round(100.0 * used / limit, 1) if limit > 0 else 0.0
|
|
231
|
+
if percent < 80.0:
|
|
232
|
+
return
|
|
233
|
+
payload: Dict[str, Any] = {
|
|
234
|
+
"type": "usage_alert",
|
|
235
|
+
"tenant_id": tenant_id,
|
|
236
|
+
"scan_type": scan_type,
|
|
237
|
+
"used": used,
|
|
238
|
+
"limit": limit,
|
|
239
|
+
"percent": percent,
|
|
240
|
+
"message": (
|
|
241
|
+
f"You have used {percent}% of your monthly {scan_type} scan limit"
|
|
242
|
+
),
|
|
243
|
+
"timestamp": _now_iso(),
|
|
244
|
+
}
|
|
245
|
+
await _broadcast(payload)
|
|
246
|
+
except Exception: # noqa: BLE001
|
|
247
|
+
logger.debug("Telemetry: failed to broadcast usage_alert", exc_info=True)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Registry accessor for the REST endpoint
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_agent_snapshots() -> List[Dict[str, Any]]:
|
|
256
|
+
"""Return the most-recent snapshot dict for every known agent."""
|
|
257
|
+
return list(_agent_registry.values())
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def clear_agent_registry() -> None:
|
|
261
|
+
"""Clear the in-memory agent registry (useful for test isolation)."""
|
|
262
|
+
_agent_registry.clear()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Team collaboration / Phase 10.4 broadcasts
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def broadcast_team_annotation(
|
|
271
|
+
finding_id: str,
|
|
272
|
+
author: str,
|
|
273
|
+
text: str,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Broadcast a ``team_annotation`` event when a user comments on a finding.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
finding_id:
|
|
280
|
+
Unique identifier of the finding being annotated.
|
|
281
|
+
author:
|
|
282
|
+
Display name or identifier of the user who added the comment.
|
|
283
|
+
text:
|
|
284
|
+
Comment body text.
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
payload: Dict[str, Any] = {
|
|
288
|
+
"type": "team_annotation",
|
|
289
|
+
"finding_id": finding_id,
|
|
290
|
+
"author": author,
|
|
291
|
+
"text": text,
|
|
292
|
+
"timestamp": _now_iso(),
|
|
293
|
+
}
|
|
294
|
+
await _broadcast(payload)
|
|
295
|
+
except Exception: # noqa: BLE001
|
|
296
|
+
logger.debug("Telemetry: failed to broadcast team_annotation", exc_info=True)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def broadcast_plan_generated(
|
|
300
|
+
plan_id: str,
|
|
301
|
+
endpoint_count: int,
|
|
302
|
+
scenario_count: int,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Broadcast a ``plan_generated`` event when an LLM test plan completes.
|
|
305
|
+
|
|
306
|
+
Parameters
|
|
307
|
+
----------
|
|
308
|
+
plan_id:
|
|
309
|
+
Unique identifier of the generated test plan.
|
|
310
|
+
endpoint_count:
|
|
311
|
+
Number of API endpoints covered by the plan.
|
|
312
|
+
scenario_count:
|
|
313
|
+
Total number of test scenarios generated.
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
payload: Dict[str, Any] = {
|
|
317
|
+
"type": "plan_generated",
|
|
318
|
+
"plan_id": plan_id,
|
|
319
|
+
"endpoint_count": endpoint_count,
|
|
320
|
+
"scenario_count": scenario_count,
|
|
321
|
+
"timestamp": _now_iso(),
|
|
322
|
+
}
|
|
323
|
+
await _broadcast(payload)
|
|
324
|
+
except Exception: # noqa: BLE001
|
|
325
|
+
logger.debug("Telemetry: failed to broadcast plan_generated", exc_info=True)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def broadcast_schedule_fired(
|
|
329
|
+
schedule_id: str,
|
|
330
|
+
schedule_name: str,
|
|
331
|
+
scan_type: str,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Broadcast a ``schedule_fired`` event when a scheduled scan is triggered.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
schedule_id:
|
|
338
|
+
Unique identifier of the schedule that fired.
|
|
339
|
+
schedule_name:
|
|
340
|
+
Human-readable name of the schedule.
|
|
341
|
+
scan_type:
|
|
342
|
+
Type of scan triggered, e.g. ``"functional"`` or ``"security"``.
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
payload: Dict[str, Any] = {
|
|
346
|
+
"type": "schedule_fired",
|
|
347
|
+
"schedule_id": schedule_id,
|
|
348
|
+
"schedule_name": schedule_name,
|
|
349
|
+
"scan_type": scan_type,
|
|
350
|
+
"timestamp": _now_iso(),
|
|
351
|
+
}
|
|
352
|
+
await _broadcast(payload)
|
|
353
|
+
except Exception: # noqa: BLE001
|
|
354
|
+
logger.debug("Telemetry: failed to broadcast schedule_fired", exc_info=True)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Discovery / Scenario Suggestion broadcasts (Phase 3)
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def broadcast_discovery_progress(
|
|
363
|
+
discovery_id: str,
|
|
364
|
+
pages_visited: int,
|
|
365
|
+
total_pages: int,
|
|
366
|
+
forms_found: int,
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Broadcast a ``discovery_progress`` event to all WebSocket clients.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
discovery_id:
|
|
373
|
+
Unique identifier for the crawl run.
|
|
374
|
+
pages_visited:
|
|
375
|
+
Number of pages visited so far.
|
|
376
|
+
total_pages:
|
|
377
|
+
Total page budget for this crawl.
|
|
378
|
+
forms_found:
|
|
379
|
+
Number of forms discovered so far.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
payload: Dict[str, Any] = {
|
|
383
|
+
"type": "discovery_progress",
|
|
384
|
+
"discovery_id": discovery_id,
|
|
385
|
+
"pages_visited": pages_visited,
|
|
386
|
+
"total_pages": total_pages,
|
|
387
|
+
"forms_found": forms_found,
|
|
388
|
+
"timestamp": _now_iso(),
|
|
389
|
+
}
|
|
390
|
+
await _broadcast(payload)
|
|
391
|
+
except Exception: # noqa: BLE001
|
|
392
|
+
logger.debug("Telemetry: failed to broadcast discovery_progress", exc_info=True)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
async def broadcast_discovery_complete(
|
|
396
|
+
discovery_id: str,
|
|
397
|
+
scenario_count: int,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Broadcast a ``discovery_complete`` event to all WebSocket clients.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
discovery_id:
|
|
404
|
+
Unique identifier for the completed crawl run.
|
|
405
|
+
scenario_count:
|
|
406
|
+
Number of scenarios generated.
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
payload: Dict[str, Any] = {
|
|
410
|
+
"type": "discovery_complete",
|
|
411
|
+
"discovery_id": discovery_id,
|
|
412
|
+
"scenario_count": scenario_count,
|
|
413
|
+
"timestamp": _now_iso(),
|
|
414
|
+
}
|
|
415
|
+
await _broadcast(payload)
|
|
416
|
+
except Exception: # noqa: BLE001
|
|
417
|
+
logger.debug("Telemetry: failed to broadcast discovery_complete", exc_info=True)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
async def broadcast_discovery_failed(
|
|
421
|
+
discovery_id: str,
|
|
422
|
+
error: str,
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Broadcast a ``discovery_failed`` event to all WebSocket clients.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
discovery_id:
|
|
429
|
+
Unique identifier for the failed crawl run.
|
|
430
|
+
error:
|
|
431
|
+
Error message describing the failure.
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
payload: Dict[str, Any] = {
|
|
435
|
+
"type": "discovery_failed",
|
|
436
|
+
"discovery_id": discovery_id,
|
|
437
|
+
"error": error,
|
|
438
|
+
"timestamp": _now_iso(),
|
|
439
|
+
}
|
|
440
|
+
await _broadcast(payload)
|
|
441
|
+
except Exception: # noqa: BLE001
|
|
442
|
+
logger.debug("Telemetry: failed to broadcast discovery_failed", exc_info=True)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
# Autonomous Test Loop broadcasts (Phase 5)
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
async def broadcast_autonomous_iteration(
|
|
451
|
+
run_id: str,
|
|
452
|
+
iteration: int,
|
|
453
|
+
passed: int,
|
|
454
|
+
failed: int,
|
|
455
|
+
flaky: int,
|
|
456
|
+
new_failures: int,
|
|
457
|
+
scenarios_executed: int,
|
|
458
|
+
coverage_pages: int,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Broadcast an ``autonomous_iteration`` event to all WebSocket clients.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
run_id:
|
|
465
|
+
Unique identifier for the autonomous run.
|
|
466
|
+
iteration:
|
|
467
|
+
Current iteration number.
|
|
468
|
+
passed:
|
|
469
|
+
Number of scenarios that passed in this iteration.
|
|
470
|
+
failed:
|
|
471
|
+
Number of scenarios that failed in this iteration.
|
|
472
|
+
flaky:
|
|
473
|
+
Number of flaky scenarios in this iteration.
|
|
474
|
+
new_failures:
|
|
475
|
+
Number of previously unseen failures discovered.
|
|
476
|
+
scenarios_executed:
|
|
477
|
+
Total scenarios executed in this iteration.
|
|
478
|
+
coverage_pages:
|
|
479
|
+
Cumulative unique pages tested so far.
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
payload: Dict[str, Any] = {
|
|
483
|
+
"type": "autonomous_iteration",
|
|
484
|
+
"data": {
|
|
485
|
+
"run_id": run_id,
|
|
486
|
+
"iteration": iteration,
|
|
487
|
+
"passed": passed,
|
|
488
|
+
"failed": failed,
|
|
489
|
+
"flaky": flaky,
|
|
490
|
+
"new_failures": new_failures,
|
|
491
|
+
"scenarios_executed": scenarios_executed,
|
|
492
|
+
"coverage_pages": coverage_pages,
|
|
493
|
+
},
|
|
494
|
+
"timestamp": _now_iso(),
|
|
495
|
+
}
|
|
496
|
+
await _broadcast(payload)
|
|
497
|
+
except Exception: # noqa: BLE001
|
|
498
|
+
logger.debug("Telemetry: failed to broadcast autonomous_iteration", exc_info=True)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
async def broadcast_autonomous_complete(
|
|
502
|
+
run_id: str,
|
|
503
|
+
stop_reason: str,
|
|
504
|
+
total_iterations: int,
|
|
505
|
+
total_passed: int,
|
|
506
|
+
total_failed: int,
|
|
507
|
+
delta: Optional[Dict[str, Any]] = None,
|
|
508
|
+
) -> None:
|
|
509
|
+
"""Broadcast an ``autonomous_complete`` event to all WebSocket clients.
|
|
510
|
+
|
|
511
|
+
Parameters
|
|
512
|
+
----------
|
|
513
|
+
run_id:
|
|
514
|
+
Unique identifier for the completed autonomous run.
|
|
515
|
+
stop_reason:
|
|
516
|
+
Why the loop stopped: ``"max_iterations"``, ``"max_duration"``,
|
|
517
|
+
``"stable"``, ``"stopped"``, or ``"error"``.
|
|
518
|
+
total_iterations:
|
|
519
|
+
Total number of iterations completed.
|
|
520
|
+
total_passed:
|
|
521
|
+
Cumulative passed scenarios across all iterations.
|
|
522
|
+
total_failed:
|
|
523
|
+
Cumulative failed scenarios across all iterations.
|
|
524
|
+
delta:
|
|
525
|
+
Optional run-over-run delta summary dict produced by
|
|
526
|
+
:class:`~mannf.core.agents.autonomous_run_differ.AutonomousRunDiffer`.
|
|
527
|
+
When provided, clients receive richer delta fields such as
|
|
528
|
+
``new_failure_count`` and ``resolved_failure_count``.
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
data: Dict[str, Any] = {
|
|
532
|
+
"run_id": run_id,
|
|
533
|
+
"stop_reason": stop_reason,
|
|
534
|
+
"total_iterations": total_iterations,
|
|
535
|
+
"total_passed": total_passed,
|
|
536
|
+
"total_failed": total_failed,
|
|
537
|
+
}
|
|
538
|
+
if delta:
|
|
539
|
+
data["delta"] = delta
|
|
540
|
+
payload: Dict[str, Any] = {
|
|
541
|
+
"type": "autonomous_complete",
|
|
542
|
+
"data": data,
|
|
543
|
+
"timestamp": _now_iso(),
|
|
544
|
+
}
|
|
545
|
+
await _broadcast(payload)
|
|
546
|
+
except Exception: # noqa: BLE001
|
|
547
|
+
logger.debug("Telemetry: failed to broadcast autonomous_complete", exc_info=True)
|