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,514 @@
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
+
5
+ """Admin API routes for tenant management.
6
+
7
+ All routes are mounted under ``/api/v1/admin`` and protected by the
8
+ ``X-Admin-Key`` header (checked against the ``NAT_ADMIN_KEY`` env var).
9
+
10
+ Endpoints:
11
+ POST /api/v1/admin/tenants — Create tenant
12
+ GET /api/v1/admin/tenants — List tenants
13
+ GET /api/v1/admin/tenants/{id} — Get tenant details
14
+ PUT /api/v1/admin/tenants/{id} — Update tenant
15
+ DELETE /api/v1/admin/tenants/{id} — Deactivate tenant
16
+ POST /api/v1/admin/tenants/{id}/rotate-key — Rotate API key
17
+ GET /api/v1/admin/tenants/{id}/usage — Get usage stats
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from typing import Optional
24
+
25
+ from fastapi import APIRouter, Header, HTTPException, Query
26
+ from pydantic import BaseModel
27
+
28
+ from mannf.product.billing.tenant_manager import TenantManager
29
+
30
+ router = APIRouter(prefix="/api/v1/admin", tags=["admin"])
31
+
32
+ _manager = TenantManager()
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Auth helper
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ def _require_admin_key(x_admin_key: Optional[str]) -> None:
41
+ """Raise 401 / 403 if the X-Admin-Key header is missing or invalid."""
42
+ admin_key = os.environ.get("NAT_ADMIN_KEY", "")
43
+ if not admin_key:
44
+ raise HTTPException(
45
+ status_code=503,
46
+ detail={
47
+ "error": {
48
+ "code": "ADMIN_NOT_CONFIGURED",
49
+ "message": "Admin API is not configured — set NAT_ADMIN_KEY.",
50
+ }
51
+ },
52
+ )
53
+ if not x_admin_key:
54
+ raise HTTPException(
55
+ status_code=401,
56
+ detail={
57
+ "error": {
58
+ "code": "MISSING_ADMIN_KEY",
59
+ "message": "X-Admin-Key header is required.",
60
+ }
61
+ },
62
+ )
63
+ if x_admin_key != admin_key:
64
+ raise HTTPException(
65
+ status_code=403,
66
+ detail={
67
+ "error": {
68
+ "code": "INVALID_ADMIN_KEY",
69
+ "message": "Invalid admin key.",
70
+ }
71
+ },
72
+ )
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Request / response models
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ class CreateTenantRequest(BaseModel):
81
+ name: str
82
+ email: str
83
+ plan: str = "free"
84
+
85
+
86
+ class UpdateTenantRequest(BaseModel):
87
+ plan: Optional[str] = None
88
+ monthly_scan_quota: Optional[int] = None
89
+ monthly_security_scan_quota: Optional[int] = None
90
+ is_active: Optional[bool] = None
91
+
92
+
93
+ class CreateWorkspaceRequest(BaseModel):
94
+ name: str
95
+ description: Optional[str] = None
96
+
97
+
98
+ class CreateProjectRequest(BaseModel):
99
+ name: str
100
+ description: Optional[str] = None
101
+ parent_project_id: Optional[str] = None
102
+
103
+
104
+ class InviteUserRequest(BaseModel):
105
+ email: str
106
+ name: str
107
+ role: str = "viewer"
108
+
109
+
110
+ class AssignRoleRequest(BaseModel):
111
+ role: str
112
+ workspace_id: Optional[str] = None
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Routes
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ @router.post("/tenants", status_code=201)
121
+ async def create_tenant(
122
+ body: CreateTenantRequest,
123
+ x_admin_key: Optional[str] = Header(default=None),
124
+ ) -> dict:
125
+ """Create a new tenant and return its API key (shown once)."""
126
+ _require_admin_key(x_admin_key)
127
+ try:
128
+ return await _manager.create_tenant(
129
+ name=body.name,
130
+ email=body.email,
131
+ plan_tier=body.plan,
132
+ )
133
+ except (RuntimeError, ValueError) as exc:
134
+ raise HTTPException(status_code=400, detail={"error": {"code": "BAD_REQUEST", "message": str(exc)}}) from exc
135
+
136
+
137
+ @router.get("/tenants")
138
+ async def list_tenants(
139
+ plan: Optional[str] = Query(default=None),
140
+ active_only: bool = Query(default=True),
141
+ limit: int = Query(default=100, ge=1, le=1000),
142
+ x_admin_key: Optional[str] = Header(default=None),
143
+ ) -> list:
144
+ """List tenants with optional plan/status filtering."""
145
+ _require_admin_key(x_admin_key)
146
+ try:
147
+ return await _manager.list_tenants(
148
+ plan_filter=plan,
149
+ active_only=active_only,
150
+ limit=limit,
151
+ )
152
+ except RuntimeError as exc:
153
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
154
+
155
+
156
+ @router.get("/tenants/{tenant_id}")
157
+ async def get_tenant(
158
+ tenant_id: str,
159
+ x_admin_key: Optional[str] = Header(default=None),
160
+ ) -> dict:
161
+ """Get tenant details by ID."""
162
+ _require_admin_key(x_admin_key)
163
+ try:
164
+ result = await _manager.get_tenant(tenant_id)
165
+ except RuntimeError as exc:
166
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
167
+ if result is None:
168
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": f"Tenant {tenant_id!r} not found."}})
169
+ return result
170
+
171
+
172
+ @router.put("/tenants/{tenant_id}")
173
+ async def update_tenant(
174
+ tenant_id: str,
175
+ body: UpdateTenantRequest,
176
+ x_admin_key: Optional[str] = Header(default=None),
177
+ ) -> dict:
178
+ """Update tenant plan, quotas, or active status."""
179
+ _require_admin_key(x_admin_key)
180
+ try:
181
+ return await _manager.update_tenant(
182
+ tenant_id=tenant_id,
183
+ plan_tier=body.plan,
184
+ monthly_scan_quota=body.monthly_scan_quota,
185
+ monthly_security_scan_quota=body.monthly_security_scan_quota,
186
+ is_active=body.is_active,
187
+ )
188
+ except KeyError as exc:
189
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
190
+ except (RuntimeError, ValueError) as exc:
191
+ raise HTTPException(status_code=400, detail={"error": {"code": "BAD_REQUEST", "message": str(exc)}}) from exc
192
+
193
+
194
+ @router.delete("/tenants/{tenant_id}")
195
+ async def deactivate_tenant(
196
+ tenant_id: str,
197
+ x_admin_key: Optional[str] = Header(default=None),
198
+ ) -> dict:
199
+ """Soft-delete a tenant — mark as inactive and revoke API keys."""
200
+ _require_admin_key(x_admin_key)
201
+ try:
202
+ return await _manager.deactivate_tenant(tenant_id)
203
+ except KeyError as exc:
204
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
205
+ except RuntimeError as exc:
206
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
207
+
208
+
209
+ @router.post("/tenants/{tenant_id}/rotate-key")
210
+ async def rotate_api_key(
211
+ tenant_id: str,
212
+ x_admin_key: Optional[str] = Header(default=None),
213
+ ) -> dict:
214
+ """Revoke the current API key and issue a new one (shown once)."""
215
+ _require_admin_key(x_admin_key)
216
+ try:
217
+ return await _manager.rotate_api_key(tenant_id)
218
+ except KeyError as exc:
219
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
220
+ except RuntimeError as exc:
221
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
222
+
223
+
224
+ @router.get("/tenants/{tenant_id}/usage")
225
+ async def get_tenant_usage(
226
+ tenant_id: str,
227
+ x_admin_key: Optional[str] = Header(default=None),
228
+ ) -> dict:
229
+ """Get current month's usage statistics for a tenant."""
230
+ _require_admin_key(x_admin_key)
231
+ try:
232
+ return await _manager.get_tenant_usage(tenant_id)
233
+ except KeyError as exc:
234
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
235
+ except RuntimeError as exc:
236
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
237
+
238
+
239
+ @router.get("/tenants/{tenant_id}/events")
240
+ async def get_tenant_billing_events(
241
+ tenant_id: str,
242
+ limit: int = Query(default=100, ge=1, le=500),
243
+ event_type: Optional[str] = Query(default=None),
244
+ x_admin_key: Optional[str] = Header(default=None),
245
+ ) -> dict:
246
+ """Get billing event audit log for a specific tenant."""
247
+ _require_admin_key(x_admin_key)
248
+ try:
249
+ from mannf.product.billing.audit import get_billing_events # noqa: PLC0415
250
+
251
+ events = await get_billing_events(tenant_id, limit=limit, event_type=event_type)
252
+ return {"tenant_id": tenant_id, "events": events, "count": len(events)}
253
+ except ValueError as exc:
254
+ raise HTTPException(status_code=400, detail={"error": {"code": "BAD_REQUEST", "message": str(exc)}}) from exc
255
+ except RuntimeError as exc:
256
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Workspace endpoints
261
+ # ---------------------------------------------------------------------------
262
+
263
+
264
+ @router.post("/tenants/{tenant_id}/workspaces", status_code=201)
265
+ async def create_workspace(
266
+ tenant_id: str,
267
+ body: CreateWorkspaceRequest,
268
+ x_admin_key: Optional[str] = Header(default=None),
269
+ ) -> dict:
270
+ """Create a workspace for a tenant."""
271
+ _require_admin_key(x_admin_key)
272
+ try:
273
+ return await _manager.create_workspace(
274
+ tenant_id=tenant_id,
275
+ name=body.name,
276
+ description=body.description,
277
+ )
278
+ except KeyError as exc:
279
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
280
+ except RuntimeError as exc:
281
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
282
+
283
+
284
+ @router.get("/tenants/{tenant_id}/workspaces")
285
+ async def list_workspaces(
286
+ tenant_id: str,
287
+ x_admin_key: Optional[str] = Header(default=None),
288
+ ) -> list:
289
+ """List workspaces for a tenant."""
290
+ _require_admin_key(x_admin_key)
291
+ try:
292
+ return await _manager.list_workspaces(tenant_id=tenant_id)
293
+ except KeyError as exc:
294
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
295
+ except RuntimeError as exc:
296
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
297
+
298
+
299
+ @router.delete("/tenants/{tenant_id}/workspaces/{workspace_id}")
300
+ async def delete_workspace(
301
+ tenant_id: str,
302
+ workspace_id: str,
303
+ x_admin_key: Optional[str] = Header(default=None),
304
+ ) -> dict:
305
+ """Deactivate a workspace."""
306
+ _require_admin_key(x_admin_key)
307
+ try:
308
+ return await _manager.delete_workspace(tenant_id=tenant_id, workspace_id=workspace_id)
309
+ except KeyError as exc:
310
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
311
+ except RuntimeError as exc:
312
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
313
+
314
+
315
+ # ---------------------------------------------------------------------------
316
+ # Project endpoints
317
+ # ---------------------------------------------------------------------------
318
+
319
+
320
+ @router.post("/tenants/{tenant_id}/workspaces/{workspace_id}/projects", status_code=201)
321
+ async def create_project(
322
+ tenant_id: str,
323
+ workspace_id: str,
324
+ body: CreateProjectRequest,
325
+ x_admin_key: Optional[str] = Header(default=None),
326
+ ) -> dict:
327
+ """Create a project in a workspace."""
328
+ _require_admin_key(x_admin_key)
329
+ try:
330
+ return await _manager.create_project(
331
+ tenant_id=tenant_id,
332
+ workspace_id=workspace_id,
333
+ name=body.name,
334
+ description=body.description,
335
+ parent_project_id=body.parent_project_id,
336
+ )
337
+ except KeyError as exc:
338
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
339
+ except (RuntimeError, ValueError) as exc:
340
+ raise HTTPException(status_code=400, detail={"error": {"code": "BAD_REQUEST", "message": str(exc)}}) from exc
341
+
342
+
343
+ @router.get("/tenants/{tenant_id}/workspaces/{workspace_id}/projects")
344
+ async def list_projects(
345
+ tenant_id: str,
346
+ workspace_id: str,
347
+ x_admin_key: Optional[str] = Header(default=None),
348
+ ) -> list:
349
+ """List projects in a workspace."""
350
+ _require_admin_key(x_admin_key)
351
+ try:
352
+ return await _manager.list_projects(tenant_id=tenant_id, workspace_id=workspace_id)
353
+ except KeyError as exc:
354
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
355
+ except RuntimeError as exc:
356
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
357
+
358
+
359
+ @router.delete("/tenants/{tenant_id}/projects/{project_id}")
360
+ async def delete_project(
361
+ tenant_id: str,
362
+ project_id: str,
363
+ x_admin_key: Optional[str] = Header(default=None),
364
+ ) -> dict:
365
+ """Deactivate a project."""
366
+ _require_admin_key(x_admin_key)
367
+ try:
368
+ return await _manager.delete_project(tenant_id=tenant_id, project_id=project_id)
369
+ except KeyError as exc:
370
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
371
+ except RuntimeError as exc:
372
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
373
+
374
+
375
+ # ---------------------------------------------------------------------------
376
+ # User endpoints
377
+ # ---------------------------------------------------------------------------
378
+
379
+
380
+ @router.post("/tenants/{tenant_id}/users", status_code=201)
381
+ async def invite_user(
382
+ tenant_id: str,
383
+ body: InviteUserRequest,
384
+ x_admin_key: Optional[str] = Header(default=None),
385
+ ) -> dict:
386
+ """Invite a user to a tenant."""
387
+ _require_admin_key(x_admin_key)
388
+ try:
389
+ return await _manager.invite_user(
390
+ tenant_id=tenant_id,
391
+ email=body.email,
392
+ name=body.name,
393
+ role=body.role,
394
+ )
395
+ except KeyError as exc:
396
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
397
+ except ValueError as exc:
398
+ raise HTTPException(status_code=422, detail={"error": {"code": "SEAT_LIMIT_EXCEEDED" if "SEAT_LIMIT_EXCEEDED" in str(exc) else "BAD_REQUEST", "message": str(exc)}}) from exc
399
+ except RuntimeError as exc:
400
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
401
+
402
+
403
+ @router.get("/tenants/{tenant_id}/users")
404
+ async def list_users(
405
+ tenant_id: str,
406
+ x_admin_key: Optional[str] = Header(default=None),
407
+ ) -> list:
408
+ """List users in a tenant."""
409
+ _require_admin_key(x_admin_key)
410
+ try:
411
+ return await _manager.list_users(tenant_id=tenant_id)
412
+ except KeyError as exc:
413
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
414
+ except RuntimeError as exc:
415
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
416
+
417
+
418
+ @router.put("/tenants/{tenant_id}/users/{user_id}/role")
419
+ async def assign_user_role(
420
+ tenant_id: str,
421
+ user_id: str,
422
+ body: AssignRoleRequest,
423
+ x_admin_key: Optional[str] = Header(default=None),
424
+ ) -> dict:
425
+ """Assign or update a user's role."""
426
+ _require_admin_key(x_admin_key)
427
+ try:
428
+ return await _manager.assign_role(
429
+ tenant_id=tenant_id,
430
+ user_id=user_id,
431
+ role=body.role,
432
+ workspace_id=body.workspace_id,
433
+ )
434
+ except KeyError as exc:
435
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
436
+ except (RuntimeError, ValueError) as exc:
437
+ raise HTTPException(status_code=400, detail={"error": {"code": "BAD_REQUEST", "message": str(exc)}}) from exc
438
+
439
+
440
+ @router.delete("/tenants/{tenant_id}/users/{user_id}")
441
+ async def remove_user(
442
+ tenant_id: str,
443
+ user_id: str,
444
+ x_admin_key: Optional[str] = Header(default=None),
445
+ ) -> dict:
446
+ """Deactivate a user."""
447
+ _require_admin_key(x_admin_key)
448
+ try:
449
+ return await _manager.remove_user(tenant_id=tenant_id, user_id=user_id)
450
+ except KeyError as exc:
451
+ raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": str(exc)}}) from exc
452
+ except RuntimeError as exc:
453
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
454
+
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # Audit log endpoint
458
+ # ---------------------------------------------------------------------------
459
+
460
+
461
+ @router.get("/audit-log")
462
+ async def get_audit_log(
463
+ tenant_id: Optional[str] = Query(default=None),
464
+ method: Optional[str] = Query(default=None),
465
+ path_prefix: Optional[str] = Query(default=None),
466
+ limit: int = Query(default=100, ge=1, le=500),
467
+ x_admin_key: Optional[str] = Header(default=None),
468
+ ) -> list:
469
+ """Query the API audit log."""
470
+ _require_admin_key(x_admin_key)
471
+ try:
472
+ from mannf.product.database import _async_session_factory, _build_engine # noqa: PLC0415
473
+ from mannf.product.models import ApiAuditLog # noqa: PLC0415
474
+ from sqlalchemy import select # noqa: PLC0415
475
+ import uuid as _uuid # noqa: PLC0415
476
+ except ImportError as exc:
477
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": str(exc)}}) from exc
478
+
479
+ if _build_engine() is None or _async_session_factory is None:
480
+ raise HTTPException(status_code=503, detail={"error": {"code": "DB_UNAVAILABLE", "message": "Database not configured"}})
481
+
482
+ async with _async_session_factory() as session:
483
+ stmt = select(ApiAuditLog).order_by(ApiAuditLog.created_at.desc()).limit(limit)
484
+
485
+ if tenant_id:
486
+ try:
487
+ tid = _uuid.UUID(tenant_id)
488
+ stmt = stmt.where(ApiAuditLog.tenant_id == tid)
489
+ except ValueError:
490
+ pass
491
+
492
+ if method:
493
+ stmt = stmt.where(ApiAuditLog.method == method.upper())
494
+
495
+ if path_prefix:
496
+ stmt = stmt.where(ApiAuditLog.path.startswith(path_prefix))
497
+
498
+ result = await session.execute(stmt)
499
+ entries = result.scalars().all()
500
+
501
+ return [
502
+ {
503
+ "id": str(e.id),
504
+ "tenant_id": str(e.tenant_id) if e.tenant_id else None,
505
+ "user_id": str(e.user_id) if e.user_id else None,
506
+ "method": e.method,
507
+ "path": e.path,
508
+ "status_code": e.status_code,
509
+ "request_summary": e.request_summary,
510
+ "source_ip": e.source_ip,
511
+ "created_at": e.created_at.isoformat() if e.created_at else None,
512
+ }
513
+ for e in entries
514
+ ]
@@ -0,0 +1,5 @@
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
+
5
+ from __future__ import annotations