@oneuptime/common 14.0.9 → 14.0.10

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 (383) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,1669 @@
1
+ import UserMiddleware from "../Middleware/UserAuthorization";
2
+ import CommonAPI from "./CommonAPI";
3
+ import Express, {
4
+ ExpressRequest,
5
+ ExpressResponse,
6
+ ExpressRouter,
7
+ NextFunction,
8
+ } from "../Utils/Express";
9
+ import Response from "../Utils/Response";
10
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
11
+ import OneUptimeDate from "../../Types/Date";
12
+ import BadDataException from "../../Types/Exception/BadDataException";
13
+ import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
14
+ import ServiceUnavailableException from "../../Types/Exception/ServiceUnavailableException";
15
+ import TooManyRequestsException from "../../Types/Exception/TooManyRequestsException";
16
+ import ObjectID from "../../Types/ObjectID";
17
+ import { JSONObject } from "../../Types/JSON";
18
+ import Permission from "../../Types/Permission";
19
+ import RunnerJobOrigin from "../../Types/Runbook/RunnerJobOrigin";
20
+ import RunnerJobStatus from "../../Types/Runbook/RunnerJobStatus";
21
+ import RunbookStepType from "../../Types/Runbook/RunbookStepType";
22
+ import AIRunType from "../../Types/AI/AIRunType";
23
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
24
+ import {
25
+ RESOURCE_AI_ACCESS_INSIGHTS_PATH,
26
+ RESOURCE_AI_ACCESS_RESET_AGENT_PATH,
27
+ RESOURCE_AI_ACCESS_STATUS_PATH,
28
+ RESOURCE_AI_ACCESS_TEST_PATH,
29
+ RESOURCE_AI_INSIGHTS_COMMAND_WINDOW_IN_DAYS,
30
+ RESOURCE_AI_INSIGHTS_LIMIT,
31
+ RESOURCE_AI_INSIGHTS_RATIONALE_MAX_LENGTH,
32
+ ResourceAiAccessTestCommandResult,
33
+ ResourceAiInsightFix,
34
+ ResourceAiInsightInvestigation,
35
+ ResourceAiInsights,
36
+ } from "../../Types/AI/ResourceAiAccessApi";
37
+ import {
38
+ RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS,
39
+ getResourceAgentName,
40
+ getResourceAiAgentResetRefusal,
41
+ getResourceSentenceName,
42
+ } from "../../Types/AI/ResourceAiAccessPermissions";
43
+ import AiResourceType, {
44
+ AI_RESOURCE_TYPE_INFO,
45
+ parseAiResourceType,
46
+ } from "../../Types/ResourceAiAgent/AiResourceType";
47
+ import {
48
+ DEFAULT_RESOURCE_COMMAND_TIMEOUT_MS,
49
+ ResourceAiAccessGap,
50
+ ResourceAiAccessGapCode,
51
+ ResourceAiAccessStatus,
52
+ } from "../../Types/ResourceAiAgent/ResourceAiAccess";
53
+ import AIRun from "../../Models/DatabaseModels/AIRun";
54
+ import Alert from "../../Models/DatabaseModels/Alert";
55
+ import AutoRemediationSuggestion from "../../Models/DatabaseModels/AutoRemediationSuggestion";
56
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
57
+ import Incident from "../../Models/DatabaseModels/Incident";
58
+ import ResourceAiAgent from "../../Models/DatabaseModels/ResourceAiAgent";
59
+ import RunnerJob from "../../Models/DatabaseModels/RunnerJob";
60
+ import GlobalCache from "../Infrastructure/GlobalCache";
61
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
62
+ import AIRunService from "../Services/AIRunService";
63
+ import AlertService from "../Services/AlertService";
64
+ import AutoRemediationSuggestionService from "../Services/AutoRemediationSuggestionService";
65
+ import CephClusterService from "../Services/CephClusterService";
66
+ import DatabaseServerService from "../Services/DatabaseServerService";
67
+ import DatabaseService from "../Services/DatabaseService";
68
+ import DockerHostService from "../Services/DockerHostService";
69
+ import DockerSwarmClusterService from "../Services/DockerSwarmClusterService";
70
+ import HostService from "../Services/HostService";
71
+ import IncidentService from "../Services/IncidentService";
72
+ import PodmanHostService from "../Services/PodmanHostService";
73
+ import ProxmoxClusterService from "../Services/ProxmoxClusterService";
74
+ import ResourceAiAccessService from "../Services/ResourceAiAccessService";
75
+ import ResourceAiAgentService from "../Services/ResourceAiAgentService";
76
+ import RunnerJobService from "../Services/RunnerJobService";
77
+ import VMwareVCenterService from "../Services/VMwareVCenterService";
78
+ import QueryHelper from "../Types/Database/QueryHelper";
79
+ import logger from "../Utils/Logger";
80
+ import { holdsAnyUnblockedPermission } from "../Utils/Runbook/RunbookExecutePermission";
81
+ import ResourceCommandJobRunner, {
82
+ RESOURCE_COMMAND_CLAIM_TIMEOUT_MS,
83
+ ResourceCommandJobOutcome,
84
+ } from "../Utils/AI/ResourceAccess/ResourceCommandJobRunner";
85
+
86
+ const router: ExpressRouter = Express.getRouter();
87
+
88
+ /*
89
+ * The custom calls behind the AI pages (AI → AI agent and AI → Insights) of
90
+ * every infrastructure resource a resource AI agent serves: Docker, Podman
91
+ * and Docker Swarm hosts, Proxmox clusters, VMware vCenters, Ceph clusters,
92
+ * database servers and hosts. The resource-agnostic sibling of
93
+ * KubernetesClusterAiAccessAPI, shaped like it. Everything else on those
94
+ * pages is ordinary CRUD on the resource model (the investigation switch,
95
+ * the remediation mode, the command allowlist — policed by
96
+ * ResourceAiAccessSettings in each resource's service).
97
+ *
98
+ * Every route takes { resourceType, resourceId } and first reads the
99
+ * resource through ITS service under the caller's own props, so the
100
+ * resource's read ACL (and any label-scoped block) applies exactly as on a
101
+ * CRUD read. That check is what guards the agent data too: the
102
+ * ResourceAiAgent table is readable by anyone who may read ANY of the eight
103
+ * resource types, so nothing here returns an agent without it.
104
+ *
105
+ * POST /resource-ai-access/status { resourceType, resourceId }
106
+ * The readiness checklist (ResourceAiAccessStatus): can OneUptime AI
107
+ * reach this resource through its AI agent, what may it do, and what is
108
+ * missing. Requires read access to the resource.
109
+ *
110
+ * POST /resource-ai-access/test { resourceType, resourceId }
111
+ * Runs the type's read-only test commands
112
+ * (AI_RESOURCE_TYPE_INFO[type].testCommands) through the resource's AI
113
+ * agent and returns their output, so an operator can see the access
114
+ * work before an incident does. Read-only, but it spends the agent's
115
+ * time, so it requires edit access to THIS resource (decided for the
116
+ * row, labels included, as a CRUD update of it would be) and has its own
117
+ * limits instead of spending the project's investigation budget: one
118
+ * test at a time per resource (an atomic reservation), a few per minute
119
+ * and a cumulative ceiling per hour per resource, and a few per minute
120
+ * per user.
121
+ *
122
+ * POST /resource-ai-access/reset-agent { resourceType, resourceId }
123
+ * Forgets the key of the resource's AI agent, so whatever holds it is
124
+ * locked out and the real agent registers afresh within a few minutes.
125
+ * For the people who may loosen AI access
126
+ * (RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS).
127
+ *
128
+ * POST /resource-ai-access/insights { resourceType, resourceId }
129
+ * What AI investigated and changed on the resource, as summaries. Same
130
+ * read gate as the status; what it says about incidents, alerts, AI runs
131
+ * and suggestions follows the caller's own read access to those (see
132
+ * getResourceAiInsights).
133
+ */
134
+
135
+ /*
136
+ * How each resource type is read: its table's service (under the caller's
137
+ * props — that is the access check) and the Incident/Alert relation that
138
+ * links it to a subject.
139
+ */
140
+ interface ResourceAiAccessKind {
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ service: DatabaseService<any>;
143
+ subjectRelation: string;
144
+ }
145
+
146
+ export const RESOURCE_AI_ACCESS_KINDS: Readonly<
147
+ Record<AiResourceType, ResourceAiAccessKind>
148
+ > = {
149
+ [AiResourceType.DockerHost]: {
150
+ service: DockerHostService,
151
+ subjectRelation: "dockerHosts",
152
+ },
153
+ [AiResourceType.PodmanHost]: {
154
+ service: PodmanHostService,
155
+ subjectRelation: "podmanHosts",
156
+ },
157
+ [AiResourceType.DockerSwarmCluster]: {
158
+ service: DockerSwarmClusterService,
159
+ subjectRelation: "dockerSwarmClusters",
160
+ },
161
+ [AiResourceType.ProxmoxCluster]: {
162
+ service: ProxmoxClusterService,
163
+ subjectRelation: "proxmoxClusters",
164
+ },
165
+ [AiResourceType.VMwareVCenter]: {
166
+ service: VMwareVCenterService,
167
+ subjectRelation: "vmwareVCenters",
168
+ },
169
+ [AiResourceType.CephCluster]: {
170
+ service: CephClusterService,
171
+ subjectRelation: "cephClusters",
172
+ },
173
+ [AiResourceType.DatabaseServer]: {
174
+ service: DatabaseServerService,
175
+ subjectRelation: "databaseServers",
176
+ },
177
+ [AiResourceType.Host]: {
178
+ service: HostService,
179
+ subjectRelation: "hosts",
180
+ },
181
+ };
182
+
183
+ // The resource a route acts on, once the caller may read it.
184
+ export interface AccessibleResource {
185
+ resourceType: AiResourceType;
186
+ id: ObjectID;
187
+ name: string;
188
+ }
189
+
190
+ async function getLoggedInProps(
191
+ req: ExpressRequest,
192
+ ): Promise<DatabaseCommonInteractionProps> {
193
+ const props: DatabaseCommonInteractionProps =
194
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
195
+
196
+ CommonAPI.assertAuthenticatedUser(props);
197
+
198
+ return { ...props, isMultiTenantRequest: false };
199
+ }
200
+
201
+ /*
202
+ * The resource type a request names: an AiResourceType value (any case) or
203
+ * an agent alias ("docker", "db", ...). Anything else is refused before any
204
+ * lookup.
205
+ */
206
+ export function readResourceType(value: unknown): AiResourceType {
207
+ const resourceType: AiResourceType | null = parseAiResourceType(value);
208
+
209
+ if (!resourceType) {
210
+ throw new BadDataException(
211
+ `resourceType is required and must be one of ${Object.values(
212
+ AiResourceType,
213
+ ).join(", ")}.`,
214
+ );
215
+ }
216
+
217
+ return resourceType;
218
+ }
219
+
220
+ /*
221
+ * Access check under the USER's permissions inside the tenant. Null is
222
+ * "does not exist OR not yours", reported identically so the route never
223
+ * leaks whether an id exists in another project.
224
+ */
225
+ async function findAccessibleResource(data: {
226
+ req: ExpressRequest;
227
+ props: DatabaseCommonInteractionProps;
228
+ tenantId: ObjectID;
229
+ }): Promise<AccessibleResource> {
230
+ const body: JSONObject = (data.req.body || {}) as JSONObject;
231
+
232
+ const resourceType: AiResourceType = readResourceType(body["resourceType"]);
233
+
234
+ const resourceIdString: string | undefined = body["resourceId"] as
235
+ | string
236
+ | undefined;
237
+
238
+ if (
239
+ !resourceIdString ||
240
+ typeof resourceIdString !== "string" ||
241
+ !ObjectID.isValidUUID(resourceIdString)
242
+ ) {
243
+ throw new BadDataException("resourceId is required.");
244
+ }
245
+
246
+ /*
247
+ * Read under the user's own props — so the resource's read ACL (and any
248
+ * label-scoped block) applies exactly as on a CRUD read — and scoped to
249
+ * the tenant in the query itself.
250
+ */
251
+ const resource: BaseModel | null = (await RESOURCE_AI_ACCESS_KINDS[
252
+ resourceType
253
+ ].service.findOneBy({
254
+ query: {
255
+ _id: resourceIdString,
256
+ projectId: data.tenantId,
257
+ },
258
+ select: { _id: true, projectId: true, name: true },
259
+ props: data.props,
260
+ })) as BaseModel | null;
261
+
262
+ const record: Record<string, unknown> | null = resource as unknown as Record<
263
+ string,
264
+ unknown
265
+ > | null;
266
+ const projectId: unknown = record ? record["projectId"] : undefined;
267
+
268
+ /*
269
+ * A row from another project is reported exactly like a missing one
270
+ * (belt and braces: the query is already tenant-scoped), so the route
271
+ * never confirms that an id exists somewhere else.
272
+ */
273
+ if (
274
+ !resource ||
275
+ !resource.id ||
276
+ !projectId ||
277
+ String(projectId) !== data.tenantId.toString()
278
+ ) {
279
+ throw new BadDataException(
280
+ `${AI_RESOURCE_TYPE_INFO[resourceType].displayName} not found (or you do not have access to it).`,
281
+ );
282
+ }
283
+
284
+ return {
285
+ resourceType,
286
+ id: resource.id,
287
+ name:
288
+ typeof record!["name"] === "string" && record!["name"]
289
+ ? (record!["name"] as string)
290
+ : resource.id.toString(),
291
+ };
292
+ }
293
+
294
+ /*
295
+ * The access test is read-only, but it spends the agent's time and prints
296
+ * what the agent can see, so it is for the people who may EDIT this
297
+ * resource — decided for this row exactly as a CRUD update of it would be
298
+ * (ResourceAiAccessService.assertCallerMayChangeResource): the update ACL,
299
+ * label-scoped allow and block rows against the row's own labels, a
300
+ * table-wide block row, and the caller's Owned scope. Holding an edit
301
+ * permission somewhere in the project is not enough: an edit grant limited
302
+ * to "staging" does not reach the "prod" host, and a block row for "prod"
303
+ * refuses it. Master admins are not gated.
304
+ */
305
+ async function assertCanEditResource(data: {
306
+ props: DatabaseCommonInteractionProps;
307
+ projectId: ObjectID;
308
+ resource: AccessibleResource;
309
+ }): Promise<void> {
310
+ try {
311
+ await ResourceAiAccessService.assertCallerMayChangeResource({
312
+ props: data.props,
313
+ projectId: data.projectId,
314
+ resourceType: data.resource.resourceType,
315
+ resourceId: data.resource.id,
316
+ });
317
+ } catch (error) {
318
+ if (error instanceof NotAuthorizedException) {
319
+ throw new NotAuthorizedException(
320
+ `You need permission to edit this ${getResourceSentenceName(
321
+ data.resource.resourceType,
322
+ )} to run its AI access test.`,
323
+ );
324
+ }
325
+
326
+ throw error;
327
+ }
328
+ }
329
+
330
+ /*
331
+ * Resetting the agent locks out whatever holds its key, and the real agent
332
+ * registers afresh: the same people who may loosen AI access may do it.
333
+ */
334
+ function assertCanResetAiAgent(data: {
335
+ props: DatabaseCommonInteractionProps;
336
+ projectId: ObjectID;
337
+ resourceType: AiResourceType;
338
+ }): void {
339
+ if (
340
+ !holdsAnyUnblockedPermission({
341
+ props: data.props,
342
+ projectId: data.projectId,
343
+ allowed: RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS,
344
+ })
345
+ ) {
346
+ throw new NotAuthorizedException(
347
+ getResourceAiAgentResetRefusal(data.resourceType),
348
+ );
349
+ }
350
+ }
351
+
352
+ async function getStatus(data: {
353
+ projectId: ObjectID;
354
+ resource: AccessibleResource;
355
+ }): Promise<ResourceAiAccessStatus> {
356
+ const status: ResourceAiAccessStatus | null =
357
+ await ResourceAiAccessService.getStatusForResource({
358
+ projectId: data.projectId,
359
+ resourceType: data.resource.resourceType,
360
+ resourceId: data.resource.id,
361
+ });
362
+
363
+ if (!status) {
364
+ throw new BadDataException(
365
+ `${AI_RESOURCE_TYPE_INFO[data.resource.resourceType].displayName} not found.`,
366
+ );
367
+ }
368
+
369
+ return status;
370
+ }
371
+
372
+ /*
373
+ * The access test's own limits. It enqueues real agent jobs and holds the
374
+ * request open while they run, so it is bounded where it is triggered and
375
+ * never counted against the project-wide investigation brake, so no amount
376
+ * of testing can starve real incident investigations:
377
+ *
378
+ * - one test at a time per resource: an atomic reservation (Redis SET NX,
379
+ * released when the test ends and expiring on its own after the longest
380
+ * a test can run), so concurrent requests cannot all read "nothing
381
+ * running" and all start. The per-resource counts below are read inside
382
+ * it, so they are exact;
383
+ * - a few per minute and a cumulative ceiling per hour per resource, and a
384
+ * few per minute per user across resources — the per-user count is read
385
+ * and the test's first command run under a per-user lock, so a user's
386
+ * concurrent requests on different resources are counted one after the
387
+ * other. Counted on the RunnerJob rows the test creates, which every API
388
+ * node shares.
389
+ *
390
+ * The numbers are a Kubernetes cluster's; the step ids, the reservation
391
+ * and the lock are this route's own, so testing a resource never counts
392
+ * against a cluster's tests or the other way round.
393
+ */
394
+ export const MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_MINUTE: number = 3;
395
+ export const MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_HOUR: number = 30;
396
+ export const MAX_RESOURCE_AI_ACCESS_TESTS_PER_USER_PER_MINUTE: number = 6;
397
+
398
+ /*
399
+ * A test job older than this is past both of its windows (claim plus
400
+ * execution, twice over), so a row a crashed request left Pending never
401
+ * blocks the next test for good — and neither does a reservation a crashed
402
+ * request never released.
403
+ */
404
+ const RESOURCE_AI_ACCESS_TEST_MAX_DURATION_MINUTES: number = 4;
405
+
406
+ export const RESOURCE_AI_ACCESS_TEST_RESERVATION_NAMESPACE: string =
407
+ "resource-ai-access-test";
408
+ export const RESOURCE_AI_ACCESS_TEST_USER_LOCK_NAMESPACE: string =
409
+ "resource-ai-access-test-user";
410
+
411
+ /*
412
+ * Held from the per-user count until the test's first command has finished.
413
+ * The lock refreshes itself while it is held; the timeout is how long it
414
+ * outlives a request that died holding it.
415
+ */
416
+ const RESOURCE_AI_ACCESS_TEST_USER_LOCK_TIMEOUT_MS: number = 30_000;
417
+ const RESOURCE_AI_ACCESS_TEST_USER_LOCK_ACQUIRE_TIMEOUT_MS: number = 15_000;
418
+
419
+ /*
420
+ * Gaps that block both capabilities but not the access test's transport:
421
+ * they are about the project's AI (switched off, no provider, no credits),
422
+ * and the test runs the agent's commands, never a model. An agent that is
423
+ * online but could not reach its resource at its last probe is tested too:
424
+ * the test's own output is exactly what says why, and a success clears it.
425
+ */
426
+ const NON_TRANSPORT_GAP_CODES: Array<ResourceAiAccessGapCode> = [
427
+ "ai_disabled_for_project",
428
+ "llm_provider_missing",
429
+ "ai_balance_insufficient",
430
+ "ai_agent_unreachable_resource",
431
+ ];
432
+
433
+ /*
434
+ * Every access-test job's stepId starts with this; the first command's
435
+ * with the "-1-" form plus the user who ran it, so a test (not a command)
436
+ * can be counted per resource and per user from the rows alone. Distinct
437
+ * from a Kubernetes cluster's "ai-access-test-" prefix, which the cluster
438
+ * route counts with startsWith.
439
+ */
440
+ export const RESOURCE_AI_ACCESS_TEST_STEP_ID_PREFIX: string =
441
+ "resource-ai-access-test-";
442
+
443
+ export function getResourceAiAccessTestStepId(data: {
444
+ commandNumber: number;
445
+ userId: ObjectID;
446
+ }): string {
447
+ return `${RESOURCE_AI_ACCESS_TEST_STEP_ID_PREFIX}${data.commandNumber}-${data.userId.toString()}`;
448
+ }
449
+
450
+ function getReservationKey(resource: AccessibleResource): string {
451
+ return `${resource.resourceType}:${resource.id.toString()}`;
452
+ }
453
+
454
+ function getAlreadyRunningMessage(resourceType: AiResourceType): string {
455
+ return `An AI access test is already running for this ${getResourceSentenceName(
456
+ resourceType,
457
+ )}. Wait for it to finish, then run it again.`;
458
+ }
459
+
460
+ /*
461
+ * Take this resource's one access-test slot, atomically, and return the
462
+ * token that releases it — or refuse: 429 while another test holds it, 503
463
+ * when the reservation cannot be checked at all (failing closed: the
464
+ * reservation is what makes "one at a time" true under concurrency).
465
+ */
466
+ async function reserveResourceForAccessTest(
467
+ resource: AccessibleResource,
468
+ ): Promise<string> {
469
+ const token: string = ObjectID.generate().toString();
470
+ let isReserved: boolean = false;
471
+
472
+ try {
473
+ isReserved = await GlobalCache.setStringIfNotExists(
474
+ RESOURCE_AI_ACCESS_TEST_RESERVATION_NAMESPACE,
475
+ getReservationKey(resource),
476
+ token,
477
+ { expiresInSeconds: RESOURCE_AI_ACCESS_TEST_MAX_DURATION_MINUTES * 60 },
478
+ );
479
+ } catch (error) {
480
+ logger.error(
481
+ `ResourceAiAccessAPI: could not reserve the AI access test of ${getReservationKey(
482
+ resource,
483
+ )}: ${error}`,
484
+ );
485
+ throw new ServiceUnavailableException(
486
+ "The AI access test could not start right now. Try again in a moment.",
487
+ );
488
+ }
489
+
490
+ if (!isReserved) {
491
+ throw new TooManyRequestsException(
492
+ getAlreadyRunningMessage(resource.resourceType),
493
+ );
494
+ }
495
+
496
+ return token;
497
+ }
498
+
499
+ // Best effort: the reservation expires on its own if this fails.
500
+ async function releaseResourceReservation(data: {
501
+ resource: AccessibleResource;
502
+ token: string;
503
+ }): Promise<void> {
504
+ try {
505
+ await GlobalCache.deleteKeyIfValue(
506
+ RESOURCE_AI_ACCESS_TEST_RESERVATION_NAMESPACE,
507
+ getReservationKey(data.resource),
508
+ data.token,
509
+ );
510
+ } catch (error) {
511
+ logger.error(
512
+ `ResourceAiAccessAPI: could not release the AI access test reservation of ${getReservationKey(
513
+ data.resource,
514
+ )}; it expires on its own: ${error}`,
515
+ );
516
+ }
517
+ }
518
+
519
+ /*
520
+ * The per-resource limits, read while this request holds the resource's
521
+ * reservation — so no other test of this resource can be counting (or
522
+ * creating rows) at the same time.
523
+ */
524
+ async function assertResourceAccessTestMayRun(data: {
525
+ projectId: ObjectID;
526
+ resource: AccessibleResource;
527
+ }): Promise<void> {
528
+ const resourceQuery: Record<string, unknown> = {
529
+ projectId: data.projectId,
530
+ resourceType: data.resource.resourceType,
531
+ resourceId: data.resource.id,
532
+ };
533
+ const sentenceName: string = getResourceSentenceName(
534
+ data.resource.resourceType,
535
+ );
536
+
537
+ const inFlight: number = (
538
+ await RunnerJobService.countBy({
539
+ query: {
540
+ ...resourceQuery,
541
+ stepId: QueryHelper.startsWith(RESOURCE_AI_ACCESS_TEST_STEP_ID_PREFIX),
542
+ status: QueryHelper.any([
543
+ RunnerJobStatus.Pending,
544
+ RunnerJobStatus.Claimed,
545
+ RunnerJobStatus.Running,
546
+ ]),
547
+ createdAt: QueryHelper.greaterThan(
548
+ OneUptimeDate.getSomeMinutesAgo(
549
+ RESOURCE_AI_ACCESS_TEST_MAX_DURATION_MINUTES,
550
+ ),
551
+ ),
552
+ } as never,
553
+ props: { isRoot: true },
554
+ })
555
+ ).toNumber();
556
+
557
+ if (inFlight > 0) {
558
+ throw new TooManyRequestsException(
559
+ getAlreadyRunningMessage(data.resource.resourceType),
560
+ );
561
+ }
562
+
563
+ const testsForResource: number = (
564
+ await RunnerJobService.countBy({
565
+ query: {
566
+ ...resourceQuery,
567
+ stepId: QueryHelper.startsWith(
568
+ `${RESOURCE_AI_ACCESS_TEST_STEP_ID_PREFIX}1-`,
569
+ ),
570
+ createdAt: QueryHelper.greaterThan(OneUptimeDate.getSomeMinutesAgo(1)),
571
+ } as never,
572
+ props: { isRoot: true },
573
+ })
574
+ ).toNumber();
575
+
576
+ if (
577
+ testsForResource >= MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_MINUTE
578
+ ) {
579
+ throw new TooManyRequestsException(
580
+ `This ${sentenceName}'s AI access was tested ${testsForResource} times in the last minute, which is its limit (${MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_MINUTE}). Try again in a minute.`,
581
+ );
582
+ }
583
+
584
+ /*
585
+ * The cumulative ceiling: access-test jobs are kept out of the project's
586
+ * investigation brake, so without this nothing would bound them over an
587
+ * hour.
588
+ */
589
+ const testsForResourceThisHour: number = (
590
+ await RunnerJobService.countBy({
591
+ query: {
592
+ ...resourceQuery,
593
+ stepId: QueryHelper.startsWith(
594
+ `${RESOURCE_AI_ACCESS_TEST_STEP_ID_PREFIX}1-`,
595
+ ),
596
+ createdAt: QueryHelper.greaterThan(OneUptimeDate.getSomeHoursAgo(1)),
597
+ } as never,
598
+ props: { isRoot: true },
599
+ })
600
+ ).toNumber();
601
+
602
+ if (
603
+ testsForResourceThisHour >=
604
+ MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_HOUR
605
+ ) {
606
+ throw new TooManyRequestsException(
607
+ `This ${sentenceName}'s AI access was tested ${testsForResourceThisHour} times in the last hour, which is its limit (${MAX_RESOURCE_AI_ACCESS_TESTS_PER_RESOURCE_PER_HOUR}). Try again later.`,
608
+ );
609
+ }
610
+ }
611
+
612
+ /*
613
+ * The per-user limit spans resources, so the resource reservation does not
614
+ * serialize it: the count is read and the test's first command run under a
615
+ * per-user lock, so a user's concurrent tests on different resources cannot
616
+ * all read the same count. The lock is held until that first command has
617
+ * finished, because ResourceCommandJobRunner.run enqueues and waits in one
618
+ * call and the job it creates is what the next count must see.
619
+ */
620
+ async function lockUserForAccessTestStart(
621
+ userId: ObjectID,
622
+ ): Promise<SemaphoreMutex> {
623
+ try {
624
+ return await Semaphore.lock({
625
+ key: userId.toString(),
626
+ namespace: RESOURCE_AI_ACCESS_TEST_USER_LOCK_NAMESPACE,
627
+ lockTimeout: RESOURCE_AI_ACCESS_TEST_USER_LOCK_TIMEOUT_MS,
628
+ acquireTimeout: RESOURCE_AI_ACCESS_TEST_USER_LOCK_ACQUIRE_TIMEOUT_MS,
629
+ });
630
+ } catch (error) {
631
+ logger.error(
632
+ `ResourceAiAccessAPI: could not take the AI access test lock of user ${userId.toString()}: ${error}`,
633
+ );
634
+ throw new TooManyRequestsException(
635
+ "Another AI access test of yours is starting right now. Try again in a moment.",
636
+ );
637
+ }
638
+ }
639
+
640
+ async function releaseUserLock(mutex: SemaphoreMutex): Promise<void> {
641
+ try {
642
+ await Semaphore.release(mutex);
643
+ } catch (error) {
644
+ logger.error(
645
+ `ResourceAiAccessAPI: could not release an AI access test user lock; it expires on its own: ${error}`,
646
+ );
647
+ }
648
+ }
649
+
650
+ async function assertUserMayStartAccessTest(data: {
651
+ projectId: ObjectID;
652
+ userId: ObjectID;
653
+ }): Promise<void> {
654
+ const testsByUser: number = (
655
+ await RunnerJobService.countBy({
656
+ query: {
657
+ projectId: data.projectId,
658
+ stepId: QueryHelper.startsWith(
659
+ getResourceAiAccessTestStepId({
660
+ commandNumber: 1,
661
+ userId: data.userId,
662
+ }),
663
+ ),
664
+ createdAt: QueryHelper.greaterThan(OneUptimeDate.getSomeMinutesAgo(1)),
665
+ },
666
+ props: { isRoot: true },
667
+ })
668
+ ).toNumber();
669
+
670
+ if (testsByUser >= MAX_RESOURCE_AI_ACCESS_TESTS_PER_USER_PER_MINUTE) {
671
+ throw new TooManyRequestsException(
672
+ `You ran ${testsByUser} AI access tests in the last minute, which is the limit (${MAX_RESOURCE_AI_ACCESS_TESTS_PER_USER_PER_MINUTE}). Try again in a minute.`,
673
+ );
674
+ }
675
+ }
676
+
677
+ /*
678
+ * One access-test command, run the way every AI resource command is
679
+ * (ResourceCommandJobRunner.run): enqueued through the resource-command
680
+ * chokepoint as an access test — the policy, the read-only rule and the
681
+ * resource's current agent still apply; the investigation switch and the
682
+ * project's investigation brake do not — then waited on, read and recorded
683
+ * exactly like an investigation's command (only a success or an ACCESS
684
+ * failure becomes the resource's "Last verified" / "Last error").
685
+ *
686
+ * Origin AiInvestigation with no AI run: an access test has none to keep
687
+ * alive.
688
+ */
689
+ async function runAccessTestCommand(data: {
690
+ projectId: ObjectID;
691
+ resource: AccessibleResource;
692
+ agentId: ObjectID;
693
+ command: string;
694
+ stepId: string;
695
+ }): Promise<ResourceCommandJobOutcome> {
696
+ return ResourceCommandJobRunner.run({
697
+ projectId: data.projectId,
698
+ origin: RunnerJobOrigin.AiInvestigation,
699
+ resourceType: data.resource.resourceType,
700
+ resourceId: data.resource.id,
701
+ targetResourceAiAgentId: data.agentId,
702
+ command: data.command,
703
+ stepId: data.stepId,
704
+ timeoutInMs: DEFAULT_RESOURCE_COMMAND_TIMEOUT_MS,
705
+ claimTimeoutInMs: RESOURCE_COMMAND_CLAIM_TIMEOUT_MS,
706
+ isAccessTest: true,
707
+ });
708
+ }
709
+
710
+ /*
711
+ * Start the test: under this user's lock, check the per-user limit and run
712
+ * the first command, so the next concurrent test of this user counts its
713
+ * job (see lockUserForAccessTestStart). The limit refusal is thrown (the
714
+ * route answers 429); a refusal from the chokepoint, or a wait that broke,
715
+ * is returned as the first command's failure.
716
+ */
717
+ async function startAccessTest(data: {
718
+ projectId: ObjectID;
719
+ resource: AccessibleResource;
720
+ userId: ObjectID;
721
+ agentId: ObjectID;
722
+ command: string;
723
+ }): Promise<{
724
+ outcome?: ResourceCommandJobOutcome | undefined;
725
+ error?: unknown;
726
+ }> {
727
+ const userLock: SemaphoreMutex = await lockUserForAccessTestStart(
728
+ data.userId,
729
+ );
730
+
731
+ try {
732
+ await assertUserMayStartAccessTest({
733
+ projectId: data.projectId,
734
+ userId: data.userId,
735
+ });
736
+
737
+ try {
738
+ const outcome: ResourceCommandJobOutcome = await runAccessTestCommand({
739
+ projectId: data.projectId,
740
+ resource: data.resource,
741
+ agentId: data.agentId,
742
+ command: data.command,
743
+ stepId: getResourceAiAccessTestStepId({
744
+ commandNumber: 1,
745
+ userId: data.userId,
746
+ }),
747
+ });
748
+
749
+ return { outcome };
750
+ } catch (error) {
751
+ return { error };
752
+ }
753
+ } finally {
754
+ await releaseUserLock(userLock);
755
+ }
756
+ }
757
+
758
+ function describeFailure(error: unknown): string {
759
+ if (error === undefined || error === null) {
760
+ return "The command could not be started.";
761
+ }
762
+
763
+ return error instanceof Error ? error.message : String(error);
764
+ }
765
+
766
+ /*
767
+ * The gap that stops the access test before anything is enqueued: one that
768
+ * blocks both capabilities and is about the transport — no agent, or an
769
+ * offline one — never one about the project's AI.
770
+ */
771
+ export function getAccessTestTransportGap(
772
+ status: ResourceAiAccessStatus,
773
+ ): ResourceAiAccessGap | undefined {
774
+ return status.gaps.find((gap: ResourceAiAccessGap): boolean => {
775
+ return (
776
+ gap.blocksInvestigation &&
777
+ gap.blocksRemediation &&
778
+ !NON_TRANSPORT_GAP_CODES.includes(gap.code)
779
+ );
780
+ });
781
+ }
782
+
783
+ router.post(
784
+ RESOURCE_AI_ACCESS_STATUS_PATH,
785
+ UserMiddleware.getUserMiddleware,
786
+ async (
787
+ req: ExpressRequest,
788
+ res: ExpressResponse,
789
+ next: NextFunction,
790
+ ): Promise<void> => {
791
+ try {
792
+ const props: DatabaseCommonInteractionProps = await getLoggedInProps(req);
793
+ const tenantId: ObjectID = CommonAPI.assertTenantScoped(props);
794
+
795
+ const resource: AccessibleResource = await findAccessibleResource({
796
+ req,
797
+ props,
798
+ tenantId,
799
+ });
800
+
801
+ const status: ResourceAiAccessStatus = await getStatus({
802
+ projectId: tenantId,
803
+ resource,
804
+ });
805
+
806
+ Response.sendJsonObjectResponse(
807
+ req,
808
+ res,
809
+ status as unknown as JSONObject,
810
+ );
811
+ return;
812
+ } catch (err) {
813
+ next(err);
814
+ return;
815
+ }
816
+ },
817
+ );
818
+
819
+ router.post(
820
+ RESOURCE_AI_ACCESS_TEST_PATH,
821
+ UserMiddleware.getUserMiddleware,
822
+ async (
823
+ req: ExpressRequest,
824
+ res: ExpressResponse,
825
+ next: NextFunction,
826
+ ): Promise<void> => {
827
+ try {
828
+ const props: DatabaseCommonInteractionProps = await getLoggedInProps(req);
829
+ const tenantId: ObjectID = CommonAPI.assertTenantScoped(props);
830
+
831
+ const resource: AccessibleResource = await findAccessibleResource({
832
+ req,
833
+ props,
834
+ tenantId,
835
+ });
836
+
837
+ await assertCanEditResource({
838
+ props,
839
+ projectId: tenantId,
840
+ resource,
841
+ });
842
+
843
+ const agentName: string = getResourceAgentName(resource.resourceType);
844
+
845
+ // One test at a time per resource: an atomic reservation, held to the end.
846
+ const reservation: string = await reserveResourceForAccessTest(resource);
847
+
848
+ try {
849
+ await assertResourceAccessTestMayRun({
850
+ projectId: tenantId,
851
+ resource,
852
+ });
853
+
854
+ const status: ResourceAiAccessStatus = await getStatus({
855
+ projectId: tenantId,
856
+ resource,
857
+ });
858
+
859
+ /*
860
+ * The test needs a reachable agent, not the investigation switch:
861
+ * an operator checks access BEFORE turning AI on. Only gaps that
862
+ * block the transport itself stop it — never one about the
863
+ * project's AI (the test runs the agent's commands, not a model).
864
+ */
865
+ const transportGap: ResourceAiAccessGap | undefined =
866
+ getAccessTestTransportGap(status);
867
+
868
+ if (transportGap || !status.agent || !status.agent.isOnline) {
869
+ Response.sendJsonObjectResponse(req, res, {
870
+ ok: false,
871
+ message: transportGap
872
+ ? `${transportGap.title}. ${transportGap.nextStep}`
873
+ : status.agent
874
+ ? `The ${agentName} is offline. Check that its container is running and can reach your OneUptime URL.`
875
+ : `Nothing can reach this ${getResourceSentenceName(
876
+ resource.resourceType,
877
+ )} yet: install the ${agentName}.`,
878
+ results: [],
879
+ status: status as unknown as JSONObject,
880
+ });
881
+ return;
882
+ }
883
+
884
+ const agentId: ObjectID = new ObjectID(status.agent.agentId);
885
+ const commands: ReadonlyArray<string> =
886
+ AI_RESOURCE_TYPE_INFO[resource.resourceType].testCommands;
887
+ const results: Array<ResourceAiAccessTestCommandResult> = [];
888
+ let allSucceeded: boolean = true;
889
+
890
+ const start: {
891
+ outcome?: ResourceCommandJobOutcome | undefined;
892
+ error?: unknown;
893
+ } = await startAccessTest({
894
+ projectId: tenantId,
895
+ resource,
896
+ userId: props.userId!,
897
+ agentId,
898
+ command: commands[0]!,
899
+ });
900
+
901
+ for (let index: number = 0; index < commands.length; index++) {
902
+ const command: string = commands[index]!;
903
+ let failure: unknown = undefined;
904
+ let outcome: ResourceCommandJobOutcome | undefined = undefined;
905
+
906
+ if (index === 0) {
907
+ outcome = start.outcome;
908
+ failure = start.error;
909
+ } else {
910
+ try {
911
+ outcome = await runAccessTestCommand({
912
+ projectId: tenantId,
913
+ resource,
914
+ agentId,
915
+ command,
916
+ stepId: getResourceAiAccessTestStepId({
917
+ commandNumber: index + 1,
918
+ userId: props.userId!,
919
+ }),
920
+ });
921
+ } catch (error) {
922
+ failure = error;
923
+ }
924
+ }
925
+
926
+ if (!outcome) {
927
+ allSucceeded = false;
928
+ results.push({
929
+ command,
930
+ succeeded: false,
931
+ exitCode: null,
932
+ output: "",
933
+ errorMessage: describeFailure(failure),
934
+ });
935
+ break;
936
+ }
937
+
938
+ allSucceeded = allSucceeded && outcome.succeeded;
939
+
940
+ results.push({
941
+ command: outcome.displayCommand || command,
942
+ succeeded: outcome.succeeded,
943
+ exitCode: outcome.exitCode ?? null,
944
+ output: outcome.output,
945
+ errorMessage: outcome.errorMessage ?? null,
946
+ });
947
+
948
+ if (!outcome.succeeded) {
949
+ break;
950
+ }
951
+ }
952
+
953
+ const refreshed: ResourceAiAccessStatus | null =
954
+ await ResourceAiAccessService.getStatusForResource({
955
+ projectId: tenantId,
956
+ resourceType: resource.resourceType,
957
+ resourceId: resource.id,
958
+ });
959
+
960
+ Response.sendJsonObjectResponse(req, res, {
961
+ ok: allSucceeded,
962
+ message: allSucceeded
963
+ ? `OneUptime AI can run commands on "${resource.name}" through the ${agentName}.`
964
+ : `The ${agentName} could not run the test commands successfully — see the command output below.`,
965
+ results: results as unknown as Array<JSONObject>,
966
+ status: (refreshed || status) as unknown as JSONObject,
967
+ });
968
+ return;
969
+ } finally {
970
+ await releaseResourceReservation({
971
+ resource,
972
+ token: reservation,
973
+ });
974
+ }
975
+ } catch (err) {
976
+ next(err);
977
+ return;
978
+ }
979
+ },
980
+ );
981
+
982
+ router.post(
983
+ RESOURCE_AI_ACCESS_RESET_AGENT_PATH,
984
+ UserMiddleware.getUserMiddleware,
985
+ async (
986
+ req: ExpressRequest,
987
+ res: ExpressResponse,
988
+ next: NextFunction,
989
+ ): Promise<void> => {
990
+ try {
991
+ const props: DatabaseCommonInteractionProps = await getLoggedInProps(req);
992
+ const tenantId: ObjectID = CommonAPI.assertTenantScoped(props);
993
+
994
+ const resource: AccessibleResource = await findAccessibleResource({
995
+ req,
996
+ props,
997
+ tenantId,
998
+ });
999
+
1000
+ assertCanResetAiAgent({
1001
+ props,
1002
+ projectId: tenantId,
1003
+ resourceType: resource.resourceType,
1004
+ });
1005
+
1006
+ const agentName: string = getResourceAgentName(resource.resourceType);
1007
+
1008
+ const agent: ResourceAiAgent | null =
1009
+ await ResourceAiAgentService.findAgentForResource({
1010
+ projectId: tenantId,
1011
+ resourceType: resource.resourceType,
1012
+ resourceId: resource.id,
1013
+ });
1014
+
1015
+ if (!agent) {
1016
+ throw new BadDataException(
1017
+ `This ${getResourceSentenceName(
1018
+ resource.resourceType,
1019
+ )} has no ${agentName} to reset.`,
1020
+ );
1021
+ }
1022
+
1023
+ await ResourceAiAgentService.resetAgent({
1024
+ projectId: tenantId,
1025
+ resourceType: resource.resourceType,
1026
+ resourceId: resource.id,
1027
+ userId: props.userId,
1028
+ });
1029
+
1030
+ const status: ResourceAiAccessStatus | null =
1031
+ await ResourceAiAccessService.getStatusForResource({
1032
+ projectId: tenantId,
1033
+ resourceType: resource.resourceType,
1034
+ resourceId: resource.id,
1035
+ });
1036
+
1037
+ Response.sendJsonObjectResponse(req, res, {
1038
+ ok: true,
1039
+ message: `The ${agentName} was reset. It reconnects on its own within a few minutes.`,
1040
+ ...(status ? { status: status as unknown as JSONObject } : {}),
1041
+ });
1042
+ return;
1043
+ } catch (err) {
1044
+ next(err);
1045
+ return;
1046
+ }
1047
+ },
1048
+ );
1049
+
1050
+ /*
1051
+ * How many of the resource's newest agent jobs the insights read to find
1052
+ * the AI runs and suggestions that ran commands on it. Each AI run runs at
1053
+ * most a handful, so this reaches well past the newest 25 of either.
1054
+ */
1055
+ const INSIGHTS_JOB_SCAN_LIMIT: number = 500;
1056
+
1057
+ // How many incidents and alerts linked to the resource the insights read.
1058
+ const INSIGHTS_LINKED_SUBJECT_LIMIT: number = 200;
1059
+
1060
+ // What the insights' merge reads off every row it unions.
1061
+ interface BaseRow {
1062
+ id?: ObjectID | null | undefined;
1063
+ createdAt?: Date | undefined;
1064
+ }
1065
+
1066
+ function toIsoString(date: Date | undefined): string | undefined {
1067
+ return date ? OneUptimeDate.toString(date) : undefined;
1068
+ }
1069
+
1070
+ function newestFirst<T extends { createdAt?: Date | undefined }>(
1071
+ rows: Array<T>,
1072
+ ): Array<T> {
1073
+ return rows.sort((a: T, b: T): number => {
1074
+ return (
1075
+ (b.createdAt ? new Date(b.createdAt).getTime() : 0) -
1076
+ (a.createdAt ? new Date(a.createdAt).getTime() : 0)
1077
+ );
1078
+ });
1079
+ }
1080
+
1081
+ /*
1082
+ * The union of several newest-first reads: one row per id, newest first,
1083
+ * at most `limit` of them (all of them without one).
1084
+ */
1085
+ function mergeNewest<T extends BaseRow>(
1086
+ groups: Array<Array<T>>,
1087
+ limit?: number | undefined,
1088
+ ): Array<T> {
1089
+ const byId: Map<string, T> = new Map<string, T>();
1090
+
1091
+ for (const rows of groups) {
1092
+ for (const row of rows) {
1093
+ const id: string | undefined = row.id?.toString();
1094
+
1095
+ if (id && !byId.has(id)) {
1096
+ byId.set(id, row);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ return newestFirst(Array.from(byId.values())).slice(0, limit);
1102
+ }
1103
+
1104
+ function getIds(
1105
+ rows: Array<{ id?: ObjectID | null | undefined }>,
1106
+ ): Array<ObjectID> {
1107
+ return rows
1108
+ .map((row: { id?: ObjectID | null | undefined }): ObjectID | undefined => {
1109
+ return row.id || undefined;
1110
+ })
1111
+ .filter((id: ObjectID | undefined): id is ObjectID => {
1112
+ return Boolean(id);
1113
+ });
1114
+ }
1115
+
1116
+ function getUniqueIds(ids: Array<ObjectID | undefined>): Array<ObjectID> {
1117
+ const byId: Map<string, ObjectID> = new Map<string, ObjectID>();
1118
+
1119
+ for (const id of ids) {
1120
+ if (id) {
1121
+ byId.set(id.toString(), id);
1122
+ }
1123
+ }
1124
+
1125
+ return Array.from(byId.values());
1126
+ }
1127
+
1128
+ /*
1129
+ * A read made under the caller's own props, which the permission layer
1130
+ * refuses outright for a role that cannot read the table at all (a
1131
+ * ReadDockerHost-only role reading Incident): that caller gets nothing from
1132
+ * it, the same as a caller whose labels or private-incident membership
1133
+ * leave no row readable.
1134
+ */
1135
+ async function readIfPermitted<T>(
1136
+ read: () => Promise<Array<T>>,
1137
+ ): Promise<Array<T>> {
1138
+ try {
1139
+ return await read();
1140
+ } catch (error) {
1141
+ if (error instanceof NotAuthorizedException) {
1142
+ return [];
1143
+ }
1144
+
1145
+ throw error;
1146
+ }
1147
+ }
1148
+
1149
+ /*
1150
+ * What OneUptime AI investigated and changed on one resource, as summaries
1151
+ * (ResourceAiInsights). The caller has already been checked for read
1152
+ * access to the resource (findAccessibleResource), which is a wider
1153
+ * audience than the incidents, alerts, AI runs and suggestions summarised
1154
+ * here, so what is said about THOSE follows the caller's own read access to
1155
+ * them:
1156
+ *
1157
+ * - incidents and alerts are read under the caller's props (tenant, labels,
1158
+ * private incidents), both to find the ones linked to the resource and to
1159
+ * name a run's subject; an investigation whose incident or alert the
1160
+ * caller cannot read is left out altogether;
1161
+ * - an investigation's TL;DR is shown with its readable incident or alert —
1162
+ * the incident's and alert's own AI investigation panel shows the whole
1163
+ * analysis to anyone who may read the subject — and, for a run with
1164
+ * neither (an AI insight's triage), only to a caller who may read AIRun;
1165
+ * - a fix's rationale is read under the caller's props, so only a caller who
1166
+ * may read that AutoRemediationSuggestion (its ACL and its incident's or
1167
+ * alert's privacy) gets it.
1168
+ *
1169
+ * The rest — which AI runs and suggestions touched the resource, their
1170
+ * status and dates, the command counts — is about the resource itself and
1171
+ * is read as root: AI runs, suggestions and agent jobs have narrower read
1172
+ * ACLs of their own (an investigation run is private to its author), so
1173
+ * reading them under the caller's props would empty the page for exactly
1174
+ * the people it is for. Nothing here returns command output, prompts or
1175
+ * command plans.
1176
+ *
1177
+ * - investigations: AI investigations that ran a command on this resource
1178
+ * (their RunnerJob rows name it), UNION investigations of incidents and
1179
+ * alerts linked to it — which also covers one whose access was not set
1180
+ * up, so it never ran a command. Newest first, at most
1181
+ * RESOURCE_AI_INSIGHTS_LIMIT.
1182
+ * - fixes: suggestions the resource's own AI remediation setting produced
1183
+ * (AutoRemediationSuggestion.resourceType / resourceId), UNION any
1184
+ * suggestion whose commands ran on this resource (a rule's round, too).
1185
+ * Newest first, same limit.
1186
+ * - commandCounts: commands in the last
1187
+ * RESOURCE_AI_INSIGHTS_COMMAND_WINDOW_IN_DAYS days. The AI agent page's
1188
+ * connection tests have no AI run behind them and are not counted.
1189
+ */
1190
+ export async function getResourceAiInsights(data: {
1191
+ projectId: ObjectID;
1192
+ resourceType: AiResourceType;
1193
+ resourceId: ObjectID;
1194
+ // The caller's own (tenant-pinned) props.
1195
+ props: DatabaseCommonInteractionProps;
1196
+ }): Promise<ResourceAiInsights> {
1197
+ const { projectId, resourceType, resourceId, props } = data;
1198
+ const limit: number = RESOURCE_AI_INSIGHTS_LIMIT;
1199
+ const subjectRelation: string =
1200
+ RESOURCE_AI_ACCESS_KINDS[resourceType].subjectRelation;
1201
+
1202
+ const jobs: Array<RunnerJob> = await RunnerJobService.findBy({
1203
+ query: {
1204
+ projectId,
1205
+ resourceType,
1206
+ resourceId,
1207
+ stepType: RunbookStepType.ResourceCommand,
1208
+ },
1209
+ select: { _id: true, aiRunId: true, autoRemediationSuggestionId: true },
1210
+ sort: { createdAt: SortOrder.Descending },
1211
+ limit: INSIGHTS_JOB_SCAN_LIMIT,
1212
+ skip: 0,
1213
+ props: { isRoot: true },
1214
+ });
1215
+
1216
+ const runIdsFromJobs: Map<string, ObjectID> = new Map<string, ObjectID>();
1217
+ const suggestionIdsFromJobs: Map<string, ObjectID> = new Map<
1218
+ string,
1219
+ ObjectID
1220
+ >();
1221
+
1222
+ for (const job of jobs) {
1223
+ if (job.aiRunId) {
1224
+ runIdsFromJobs.set(job.aiRunId.toString(), job.aiRunId);
1225
+ }
1226
+
1227
+ if (job.autoRemediationSuggestionId) {
1228
+ suggestionIdsFromJobs.set(
1229
+ job.autoRemediationSuggestionId.toString(),
1230
+ job.autoRemediationSuggestionId,
1231
+ );
1232
+ }
1233
+ }
1234
+
1235
+ // Only the incidents and alerts the caller may read (see above).
1236
+ const [linkedIncidents, linkedAlerts]: [Array<Incident>, Array<Alert>] =
1237
+ await Promise.all([
1238
+ readIfPermitted<Incident>(() => {
1239
+ return IncidentService.findBy({
1240
+ query: {
1241
+ projectId,
1242
+ [subjectRelation]: QueryHelper.inRelationArray([resourceId]),
1243
+ } as never,
1244
+ select: { _id: true },
1245
+ sort: { createdAt: SortOrder.Descending },
1246
+ limit: INSIGHTS_LINKED_SUBJECT_LIMIT,
1247
+ skip: 0,
1248
+ props,
1249
+ });
1250
+ }),
1251
+ readIfPermitted<Alert>(() => {
1252
+ return AlertService.findBy({
1253
+ query: {
1254
+ projectId,
1255
+ [subjectRelation]: QueryHelper.inRelationArray([resourceId]),
1256
+ } as never,
1257
+ select: { _id: true },
1258
+ sort: { createdAt: SortOrder.Descending },
1259
+ limit: INSIGHTS_LINKED_SUBJECT_LIMIT,
1260
+ skip: 0,
1261
+ props,
1262
+ });
1263
+ }),
1264
+ ]);
1265
+
1266
+ const investigations: Array<ResourceAiInsightInvestigation> =
1267
+ await getInsightInvestigations({
1268
+ projectId,
1269
+ props,
1270
+ runIds: Array.from(runIdsFromJobs.values()),
1271
+ incidentIds: getIds(linkedIncidents),
1272
+ alertIds: getIds(linkedAlerts),
1273
+ limit,
1274
+ });
1275
+
1276
+ const fixes: Array<ResourceAiInsightFix> = await getInsightFixes({
1277
+ projectId,
1278
+ props,
1279
+ resourceType,
1280
+ resourceId,
1281
+ suggestionIds: Array.from(suggestionIdsFromJobs.values()),
1282
+ limit,
1283
+ });
1284
+
1285
+ const since: Date = OneUptimeDate.getSomeDaysAgo(
1286
+ RESOURCE_AI_INSIGHTS_COMMAND_WINDOW_IN_DAYS,
1287
+ );
1288
+
1289
+ const countCommands: (
1290
+ query: Record<string, unknown>,
1291
+ ) => Promise<number> = async (
1292
+ query: Record<string, unknown>,
1293
+ ): Promise<number> => {
1294
+ return (
1295
+ await RunnerJobService.countBy({
1296
+ query: {
1297
+ ...query,
1298
+ projectId,
1299
+ resourceType,
1300
+ resourceId,
1301
+ stepType: RunbookStepType.ResourceCommand,
1302
+ createdAt: QueryHelper.greaterThan(since),
1303
+ } as never,
1304
+ props: { isRoot: true },
1305
+ })
1306
+ ).toNumber();
1307
+ };
1308
+
1309
+ const [investigationCommands, remediationCommands]: [number, number] =
1310
+ await Promise.all([
1311
+ countCommands({
1312
+ origin: RunnerJobOrigin.AiInvestigation,
1313
+ // Connection tests have no AI run; every investigation command does.
1314
+ aiRunId: QueryHelper.notNull(),
1315
+ }),
1316
+ countCommands({ origin: RunnerJobOrigin.AiRemediation }),
1317
+ ]);
1318
+
1319
+ return {
1320
+ resourceType,
1321
+ resourceId: resourceId.toString(),
1322
+ investigations,
1323
+ fixes,
1324
+ commandCounts: {
1325
+ investigation: investigationCommands,
1326
+ remediation: remediationCommands,
1327
+ },
1328
+ };
1329
+ }
1330
+
1331
+ const INSIGHT_RUN_SELECT: Record<string, boolean> = {
1332
+ _id: true,
1333
+ status: true,
1334
+ analysisTldr: true,
1335
+ createdAt: true,
1336
+ completedAt: true,
1337
+ triggeredByIncidentId: true,
1338
+ triggeredByAlertId: true,
1339
+ };
1340
+
1341
+ /*
1342
+ * Who may read the TL;DR of a run that has no incident or alert to decide
1343
+ * it: the people who may read that column of AIRun.
1344
+ */
1345
+ function getSubjectlessRunTldrReadPermissions(): Array<Permission> {
1346
+ return new AIRun().getColumnAccessControlFor("analysisTldr")?.read || [];
1347
+ }
1348
+
1349
+ async function getInsightInvestigations(data: {
1350
+ projectId: ObjectID;
1351
+ props: DatabaseCommonInteractionProps;
1352
+ runIds: Array<ObjectID>;
1353
+ incidentIds: Array<ObjectID>;
1354
+ alertIds: Array<ObjectID>;
1355
+ limit: number;
1356
+ }): Promise<Array<ResourceAiInsightInvestigation>> {
1357
+ const reads: Array<Promise<Array<AIRun>>> = [];
1358
+
1359
+ const readRuns: (query: Record<string, unknown>) => Promise<Array<AIRun>> = (
1360
+ query: Record<string, unknown>,
1361
+ ): Promise<Array<AIRun>> => {
1362
+ return AIRunService.findBy({
1363
+ query: {
1364
+ ...query,
1365
+ projectId: data.projectId,
1366
+ runType: AIRunType.Investigation,
1367
+ } as never,
1368
+ select: INSIGHT_RUN_SELECT,
1369
+ sort: { createdAt: SortOrder.Descending },
1370
+ limit: data.limit,
1371
+ skip: 0,
1372
+ props: { isRoot: true },
1373
+ });
1374
+ };
1375
+
1376
+ if (data.runIds.length > 0) {
1377
+ reads.push(readRuns({ _id: QueryHelper.any(data.runIds) }));
1378
+ }
1379
+
1380
+ if (data.incidentIds.length > 0) {
1381
+ reads.push(
1382
+ readRuns({ triggeredByIncidentId: QueryHelper.any(data.incidentIds) }),
1383
+ );
1384
+ }
1385
+
1386
+ if (data.alertIds.length > 0) {
1387
+ reads.push(
1388
+ readRuns({ triggeredByAlertId: QueryHelper.any(data.alertIds) }),
1389
+ );
1390
+ }
1391
+
1392
+ if (reads.length === 0) {
1393
+ return [];
1394
+ }
1395
+
1396
+ // Every candidate, newest first: the limit applies after the subject check.
1397
+ const candidates: Array<AIRun> = mergeNewest(await Promise.all(reads));
1398
+
1399
+ const incidentIds: Array<ObjectID> = getUniqueIds(
1400
+ candidates.map((run: AIRun): ObjectID | undefined => {
1401
+ return run.triggeredByIncidentId;
1402
+ }),
1403
+ );
1404
+ const alertIds: Array<ObjectID> = getUniqueIds(
1405
+ candidates.map((run: AIRun): ObjectID | undefined => {
1406
+ return run.triggeredByAlertId;
1407
+ }),
1408
+ );
1409
+
1410
+ /*
1411
+ * The subjects under the CALLER's props: tenant, labels and private
1412
+ * incidents apply exactly as on a CRUD read, and a role that may not read
1413
+ * incidents (or alerts) at all reads none.
1414
+ */
1415
+ const [incidents, alerts]: [Array<Incident>, Array<Alert>] =
1416
+ await Promise.all([
1417
+ incidentIds.length > 0
1418
+ ? readIfPermitted<Incident>(() => {
1419
+ return IncidentService.findBy({
1420
+ query: {
1421
+ projectId: data.projectId,
1422
+ _id: QueryHelper.any(incidentIds),
1423
+ },
1424
+ select: { _id: true, title: true, incidentNumber: true },
1425
+ limit: incidentIds.length,
1426
+ skip: 0,
1427
+ props: data.props,
1428
+ });
1429
+ })
1430
+ : Promise.resolve([]),
1431
+ alertIds.length > 0
1432
+ ? readIfPermitted<Alert>(() => {
1433
+ return AlertService.findBy({
1434
+ query: {
1435
+ projectId: data.projectId,
1436
+ _id: QueryHelper.any(alertIds),
1437
+ },
1438
+ select: { _id: true, title: true },
1439
+ limit: alertIds.length,
1440
+ skip: 0,
1441
+ props: data.props,
1442
+ });
1443
+ })
1444
+ : Promise.resolve([]),
1445
+ ]);
1446
+
1447
+ const incidentsById: Map<string, Incident> = new Map<string, Incident>(
1448
+ incidents.map((incident: Incident): [string, Incident] => {
1449
+ return [incident.id?.toString() || "", incident];
1450
+ }),
1451
+ );
1452
+ const alertsById: Map<string, Alert> = new Map<string, Alert>(
1453
+ alerts.map((alert: Alert): [string, Alert] => {
1454
+ return [alert.id?.toString() || "", alert];
1455
+ }),
1456
+ );
1457
+
1458
+ // A run whose incident or alert the caller cannot read is left out.
1459
+ const runs: Array<AIRun> = candidates
1460
+ .filter((run: AIRun): boolean => {
1461
+ if (
1462
+ run.triggeredByIncidentId &&
1463
+ !incidentsById.has(run.triggeredByIncidentId.toString())
1464
+ ) {
1465
+ return false;
1466
+ }
1467
+
1468
+ if (
1469
+ run.triggeredByAlertId &&
1470
+ !alertsById.has(run.triggeredByAlertId.toString())
1471
+ ) {
1472
+ return false;
1473
+ }
1474
+
1475
+ return true;
1476
+ })
1477
+ .slice(0, data.limit);
1478
+
1479
+ const mayReadSubjectlessTldr: boolean = holdsAnyUnblockedPermission({
1480
+ props: data.props,
1481
+ projectId: data.projectId,
1482
+ allowed: getSubjectlessRunTldrReadPermissions(),
1483
+ });
1484
+
1485
+ return runs.map((run: AIRun): ResourceAiInsightInvestigation => {
1486
+ const incident: Incident | undefined = run.triggeredByIncidentId
1487
+ ? incidentsById.get(run.triggeredByIncidentId.toString())
1488
+ : undefined;
1489
+ const alert: Alert | undefined = run.triggeredByAlertId
1490
+ ? alertsById.get(run.triggeredByAlertId.toString())
1491
+ : undefined;
1492
+
1493
+ /*
1494
+ * Every run left here with a subject has a subject the caller may read,
1495
+ * and the subject's own AI panel shows its analysis to them.
1496
+ */
1497
+ const mayReadTldr: boolean =
1498
+ Boolean(run.triggeredByIncidentId || run.triggeredByAlertId) ||
1499
+ mayReadSubjectlessTldr;
1500
+
1501
+ return {
1502
+ aiRunId: run.id!.toString(),
1503
+ status: run.status,
1504
+ analysisTldr: mayReadTldr ? run.analysisTldr || undefined : undefined,
1505
+ createdAt: toIsoString(run.createdAt),
1506
+ completedAt: toIsoString(run.completedAt),
1507
+ incident: run.triggeredByIncidentId
1508
+ ? {
1509
+ id: run.triggeredByIncidentId.toString(),
1510
+ title: incident?.title,
1511
+ number: incident?.incidentNumber,
1512
+ }
1513
+ : undefined,
1514
+ alert: run.triggeredByAlertId
1515
+ ? {
1516
+ id: run.triggeredByAlertId.toString(),
1517
+ title: alert?.title,
1518
+ }
1519
+ : undefined,
1520
+ };
1521
+ });
1522
+ }
1523
+
1524
+ /*
1525
+ * Read as root, so never the rationale: that is read under the caller's
1526
+ * props (getInsightFixes).
1527
+ */
1528
+ const INSIGHT_FIX_SELECT: Record<string, boolean> = {
1529
+ _id: true,
1530
+ status: true,
1531
+ executionMode: true,
1532
+ suggestionType: true,
1533
+ createdAt: true,
1534
+ approvedAt: true,
1535
+ incidentId: true,
1536
+ alertId: true,
1537
+ };
1538
+
1539
+ async function getInsightFixes(data: {
1540
+ projectId: ObjectID;
1541
+ props: DatabaseCommonInteractionProps;
1542
+ resourceType: AiResourceType;
1543
+ resourceId: ObjectID;
1544
+ suggestionIds: Array<ObjectID>;
1545
+ limit: number;
1546
+ }): Promise<Array<ResourceAiInsightFix>> {
1547
+ const readSuggestions: (
1548
+ query: Record<string, unknown>,
1549
+ ) => Promise<Array<AutoRemediationSuggestion>> = (
1550
+ query: Record<string, unknown>,
1551
+ ): Promise<Array<AutoRemediationSuggestion>> => {
1552
+ return AutoRemediationSuggestionService.findBy({
1553
+ query: { ...query, projectId: data.projectId } as never,
1554
+ select: INSIGHT_FIX_SELECT,
1555
+ sort: { createdAt: SortOrder.Descending },
1556
+ limit: data.limit,
1557
+ skip: 0,
1558
+ props: { isRoot: true },
1559
+ });
1560
+ };
1561
+
1562
+ const reads: Array<Promise<Array<AutoRemediationSuggestion>>> = [
1563
+ readSuggestions({
1564
+ resourceType: data.resourceType,
1565
+ resourceId: data.resourceId,
1566
+ }),
1567
+ ];
1568
+
1569
+ if (data.suggestionIds.length > 0) {
1570
+ reads.push(readSuggestions({ _id: QueryHelper.any(data.suggestionIds) }));
1571
+ }
1572
+
1573
+ const suggestions: Array<AutoRemediationSuggestion> = mergeNewest(
1574
+ await Promise.all(reads),
1575
+ data.limit,
1576
+ );
1577
+ const ids: Array<ObjectID> = getIds(suggestions);
1578
+
1579
+ /*
1580
+ * The rationale quotes the AI's analysis of the incident or alert, so it
1581
+ * is read under the CALLER's props: the suggestion's read ACL and its
1582
+ * subject's privacy decide it, as on a CRUD read.
1583
+ */
1584
+ const readable: Array<AutoRemediationSuggestion> =
1585
+ ids.length > 0
1586
+ ? await readIfPermitted<AutoRemediationSuggestion>(() => {
1587
+ return AutoRemediationSuggestionService.findBy({
1588
+ query: {
1589
+ projectId: data.projectId,
1590
+ _id: QueryHelper.any(ids),
1591
+ } as never,
1592
+ select: { _id: true, rationaleMarkdown: true },
1593
+ limit: ids.length,
1594
+ skip: 0,
1595
+ props: data.props,
1596
+ });
1597
+ })
1598
+ : [];
1599
+
1600
+ const rationaleById: Map<string, string> = new Map<string, string>();
1601
+
1602
+ for (const suggestion of readable) {
1603
+ if (suggestion.id && suggestion.rationaleMarkdown) {
1604
+ rationaleById.set(suggestion.id.toString(), suggestion.rationaleMarkdown);
1605
+ }
1606
+ }
1607
+
1608
+ return suggestions.map(
1609
+ (suggestion: AutoRemediationSuggestion): ResourceAiInsightFix => {
1610
+ const rationale: string | undefined = rationaleById.get(
1611
+ suggestion.id!.toString(),
1612
+ );
1613
+
1614
+ return {
1615
+ id: suggestion.id!.toString(),
1616
+ status: suggestion.status,
1617
+ executionMode: suggestion.executionMode,
1618
+ suggestionType: suggestion.suggestionType,
1619
+ rationale: rationale
1620
+ ? rationale.slice(0, RESOURCE_AI_INSIGHTS_RATIONALE_MAX_LENGTH)
1621
+ : undefined,
1622
+ createdAt: toIsoString(suggestion.createdAt),
1623
+ approvedAt: toIsoString(suggestion.approvedAt),
1624
+ incidentId: suggestion.incidentId?.toString(),
1625
+ alertId: suggestion.alertId?.toString(),
1626
+ };
1627
+ },
1628
+ );
1629
+ }
1630
+
1631
+ router.post(
1632
+ RESOURCE_AI_ACCESS_INSIGHTS_PATH,
1633
+ UserMiddleware.getUserMiddleware,
1634
+ async (
1635
+ req: ExpressRequest,
1636
+ res: ExpressResponse,
1637
+ next: NextFunction,
1638
+ ): Promise<void> => {
1639
+ try {
1640
+ const props: DatabaseCommonInteractionProps = await getLoggedInProps(req);
1641
+ const tenantId: ObjectID = CommonAPI.assertTenantScoped(props);
1642
+
1643
+ const resource: AccessibleResource = await findAccessibleResource({
1644
+ req,
1645
+ props,
1646
+ tenantId,
1647
+ });
1648
+
1649
+ const insights: ResourceAiInsights = await getResourceAiInsights({
1650
+ projectId: tenantId,
1651
+ resourceType: resource.resourceType,
1652
+ resourceId: resource.id,
1653
+ props,
1654
+ });
1655
+
1656
+ Response.sendJsonObjectResponse(
1657
+ req,
1658
+ res,
1659
+ insights as unknown as JSONObject,
1660
+ );
1661
+ return;
1662
+ } catch (err) {
1663
+ next(err);
1664
+ return;
1665
+ }
1666
+ },
1667
+ );
1668
+
1669
+ export default router;