@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
@@ -23,11 +23,22 @@ import {
23
23
  INLINE_COMMAND_STEP_ID_PREFIX,
24
24
  KUBECTL_ALWAYS_ASKS_SUMMARY,
25
25
  KUBECTL_SAFE_CHANGES_SUMMARY,
26
+ RESOURCE_ALWAYS_ASKS_SUMMARY,
27
+ RESOURCE_SAFE_CHANGES_SUMMARY,
26
28
  } from "../../Types/AutoRemediation/AiRemediationCommandPlan";
27
29
  import {
28
30
  KubernetesAiRemediationMode,
29
31
  KubernetesClusterAiAccessStatus,
30
32
  } from "../../Types/Kubernetes/KubernetesClusterAiAccess";
33
+ import AiResourceType, {
34
+ AI_RESOURCE_TYPE_INFO,
35
+ ALL_AI_RESOURCE_TYPES,
36
+ isAiResourceType,
37
+ } from "../../Types/ResourceAiAgent/AiResourceType";
38
+ import {
39
+ ResourceAiAccessStatus,
40
+ ResourceAiRemediationMode,
41
+ } from "../../Types/ResourceAiAgent/ResourceAiAccess";
31
42
  import RunnerJobOrigin from "../../Types/Runbook/RunnerJobOrigin";
32
43
  import { Indigo500 } from "../../Types/BrandColors";
33
44
  import OneUptimeDate from "../../Types/Date";
@@ -41,6 +52,9 @@ import AutoRemediationSuggestionService from "./AutoRemediationSuggestionService
41
52
  import IncidentFeedService from "./IncidentFeedService";
42
53
  import KubernetesClusterAiAccessService from "./KubernetesClusterAiAccessService";
43
54
  import LlmProviderService from "./LlmProviderService";
55
+ import ResourceAiAccessService, {
56
+ describeResourceNoun,
57
+ } from "./ResourceAiAccessService";
44
58
  import ProjectService from "./ProjectService";
45
59
  import RunbookRuleEngineService from "./RunbookRuleEngineService";
46
60
  import RunnerJobService from "./RunnerJobService";
@@ -84,6 +98,14 @@ export const DEFAULT_VERIFICATION_WINDOW_MINUTES: number = 15;
84
98
  */
85
99
  export const MAX_CLUSTER_REMEDIATION_ROUNDS_PER_SUBJECT: number = 2;
86
100
 
101
+ /*
102
+ * - Resource-level remediation (the AI page of a Docker or Podman host, a
103
+ * Docker Swarm, Proxmox, VMware or Ceph cluster, a database server or a
104
+ * host — no rule) gets the same two rounds per subject per resource: the
105
+ * first plan, then one follow-up when verification fails.
106
+ */
107
+ export const MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT: number = 2;
108
+
87
109
  /*
88
110
  * Upper bound on the rows the per-cluster circuit breaker and the in-flight
89
111
  * round check read for one cluster. The project-wide hourly AI-command cap
@@ -122,6 +144,30 @@ const HELD_ROUND_LOOKBACK_HOURS: number = 24;
122
144
  const UNATTENDED_ROUND_PROPOSAL_FEED_SENTENCE: string =
123
145
  "If the hourly circuit breaker for this cluster trips, or another unattended OneUptime AI round already holds the cluster (it is still changing it, or its fix is still being verified), this round becomes a proposal for your approval instead.";
124
146
 
147
+ /*
148
+ * The same clause for a resource round, in the words of
149
+ * RESOURCE_UNATTENDED_ROUND_BECOMES_PROPOSAL_SUMMARY.
150
+ */
151
+ function getResourceRoundProposalFeedSentence(noun: string): string {
152
+ return `If the hourly circuit breaker for this ${noun} trips, or another unattended OneUptime AI round already holds the ${noun} (it is still changing it, or its fix is still being verified), this round becomes a proposal for your approval instead.`;
153
+ }
154
+
155
+ /*
156
+ * The per-resource breaker lock: the rule engine's breaker is read under it
157
+ * by the command toolkit before a run's first inline change on a resource
158
+ * (RemediationCommandToolkit.reserveResourceSlot), in its own namespace so
159
+ * a resource's key can never collide with a cluster's.
160
+ */
161
+ export const RESOURCE_BREAKER_LOCK_NAMESPACE: string =
162
+ "AutoRemediationResourceBreaker";
163
+
164
+ export function getResourceBreakerLockKey(
165
+ resourceType: AiResourceType,
166
+ resourceId: string,
167
+ ): string {
168
+ return `${resourceType}:${resourceId.toLowerCase()}`;
169
+ }
170
+
125
171
  // "a write ..." -> "A write ..." for copy that starts a sentence.
126
172
  function capitalizeFirst(text: string): string {
127
173
  return text.charAt(0).toUpperCase() + text.slice(1);
@@ -206,6 +252,73 @@ export function parseClusterRoundNameSnapshot(
206
252
  return { clusterName: match[1] || "" };
207
253
  }
208
254
 
255
+ /*
256
+ * The resource-level siblings of the cluster breaker and hold types: the
257
+ * same shapes, keyed by (resourceType, resourceId) instead of a cluster.
258
+ */
259
+ export type ResourceBreakerState = ClusterBreakerState;
260
+ export type ResourceRoundReference = ClusterRoundReference;
261
+ export type ResourceRoundHold = ClusterRoundHold;
262
+
263
+ /*
264
+ * The name a resource-level round carries: 'AI remediation for Docker host
265
+ * "web-1"', 'AI remediation for host "web-2" (round 2)'. Server-written.
266
+ * Never "for cluster": parseClusterRoundNameSnapshot must not read a
267
+ * resource round as a deleted cluster's.
268
+ */
269
+ export function getResourceRoundNameSnapshot(
270
+ resourceType: AiResourceType,
271
+ resourceName: string,
272
+ round: number,
273
+ ): string {
274
+ return `AI remediation for ${describeResourceNoun(resourceType)} "${resourceName}"${
275
+ round > 1 ? ` (round ${round})` : ""
276
+ }`;
277
+ }
278
+
279
+ const RESOURCE_ROUND_NAME_SUFFIX: RegExp = /" \(round (\d+)\)$/;
280
+
281
+ /*
282
+ * The round a resource round's server-written name carries (see
283
+ * getResourceRoundNameSnapshot): N for '... "web-1" (round N)', 1 for
284
+ * anything else. The suffix always follows the closing quote, so a
285
+ * resource named 'web (round 2)' is never read as a round 2.
286
+ */
287
+ export function parseResourceRoundNumber(
288
+ ruleNameSnapshot: string | undefined | null,
289
+ ): number {
290
+ const match: RegExpExecArray | null = RESOURCE_ROUND_NAME_SUFFIX.exec(
291
+ ruleNameSnapshot || "",
292
+ );
293
+
294
+ const round: number = match ? parseInt(match[1]!, 10) : 1;
295
+
296
+ return Number.isFinite(round) && round > 1 ? round : 1;
297
+ }
298
+
299
+ /*
300
+ * Does a resource on this mode run this round of a signal unattended?
301
+ * Bypass approval runs every round unattended; Automatic runs only the
302
+ * signal's FIRST round unattended and asks for its follow-ups; every other
303
+ * mode asks. The rule engine announces a round by it, and the execution
304
+ * runner and the toolkit re-check it against the LIVE mode — a follow-up
305
+ * announced under Bypass approval asks once the resource is on Automatic.
306
+ */
307
+ export function doesResourceModeRunRoundUnattended(
308
+ mode: ResourceAiRemediationMode | undefined | null,
309
+ round: number,
310
+ ): boolean {
311
+ return (
312
+ mode === ResourceAiRemediationMode.BypassApproval ||
313
+ (round <= 1 && mode === ResourceAiRemediationMode.Automatic)
314
+ );
315
+ }
316
+
317
+ // 'Docker host "web-1"' — how a resource round names its resource in copy.
318
+ function describeResourceForFeed(status: ResourceAiAccessStatus): string {
319
+ return `${describeResourceNoun(status.resourceType)} "${status.resourceName}"`;
320
+ }
321
+
209
322
  // Is `row` ordered before the reference (createdAt, then id as tie-break)?
210
323
  function isRoundOrderedBefore(
211
324
  row: AutoRemediationSuggestion,
@@ -226,6 +339,19 @@ function isRoundOrderedBefore(
226
339
  return (row.id?.toString() || "") < (reference.suggestionId.toString() || "");
227
340
  }
228
341
 
342
+ /*
343
+ * Does any of these suggestions belong to a resource-level round? A signal
344
+ * with one is in the resource lane, so the cluster lane must not start a
345
+ * round on it (one AI fix lane per signal).
346
+ */
347
+ function hasResourceRound(
348
+ suggestions: Array<AutoRemediationSuggestion>,
349
+ ): boolean {
350
+ return suggestions.some((suggestion: AutoRemediationSuggestion): boolean => {
351
+ return Boolean(suggestion.resourceId);
352
+ });
353
+ }
354
+
229
355
  function isSameSubject(
230
356
  row: AutoRemediationSuggestion,
231
357
  subject: SubjectLinkage,
@@ -543,138 +669,430 @@ class AutoRemediationRuleEngineServiceClass {
543
669
  }
544
670
 
545
671
  /*
546
- * Whether one other run holds the cluster — see findRoundHoldingCluster.
547
- * `hasChangedCluster`: the run already has a kubectl job on the cluster,
548
- * so while it is still running it holds whatever its order or mode.
672
+ * The per-resource hourly circuit breaker — getClusterBreakerState for an
673
+ * infrastructure resource reached through its resource AI agent, with the
674
+ * same limit (MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR) and the same three
675
+ * counts, the larger winning:
676
+ * - resource-level rounds already settled AutoExecuted (their suggestion
677
+ * carries the resource's type and id);
678
+ * - distinct AI runs that executed at least one inline `ai-command-*`
679
+ * job on the resource (RunnerJob.resourceType/resourceId);
680
+ * - unattended resource rounds still IN FLIGHT: Planning with a
681
+ * FullAuto snapshot, created before the asking round.
682
+ * In-flight rows are read first, for the same reason as the cluster's.
683
+ *
684
+ * Throws on a failed read: every caller fails safe to "no headroom".
549
685
  */
550
- private getHold(data: {
551
- row: AutoRemediationSuggestion;
552
- rowId: string;
553
- hasChangedCluster: boolean;
554
- anyOrder: boolean;
555
- forRound?: ClusterRoundReference | undefined;
556
- subject?: SubjectLinkage | undefined;
557
- }): ClusterRoundHold | null {
558
- const { row, rowId } = data;
559
- const now: number = OneUptimeDate.getCurrentDate().getTime();
560
- const inFlightSince: number = OneUptimeDate.getSomeHoursAgo(
561
- IN_FLIGHT_ROUND_WINDOW_HOURS,
562
- ).getTime();
563
-
564
- const sameSubject: boolean = data.subject
565
- ? isSameSubject(row, data.subject)
566
- : false;
686
+ @CaptureSpan()
687
+ public async getResourceBreakerState(data: {
688
+ resourceType: AiResourceType;
689
+ resourceId: string;
690
+ projectId?: ObjectID | undefined;
691
+ forRound?: ResourceRoundReference | undefined;
692
+ }): Promise<ResourceBreakerState> {
693
+ const since: Date = OneUptimeDate.getSomeHoursAgo(1);
694
+ const resourceId: ObjectID = new ObjectID(data.resourceId);
695
+ const selfId: string | undefined = data.forRound?.suggestionId.toString();
567
696
 
568
- if (
569
- (row.status === AutoRemediationSuggestionStatus.AutoExecuted ||
570
- row.status === AutoRemediationSuggestionStatus.Approved) &&
571
- row.verificationStatus === AutoRemediationVerificationStatus.Pending
572
- ) {
573
- const deadline: number | null = row.verificationDeadlineAt
574
- ? new Date(row.verificationDeadlineAt).getTime()
575
- : null;
697
+ const inFlightRounds: Array<AutoRemediationSuggestion> =
698
+ await AutoRemediationSuggestionService.findBy({
699
+ query: {
700
+ ...(data.projectId ? { projectId: data.projectId } : {}),
701
+ resourceType: data.resourceType,
702
+ resourceId,
703
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
704
+ status: AutoRemediationSuggestionStatus.Planning,
705
+ executionMode: AutoRemediationExecutionMode.FullAuto,
706
+ createdAt: QueryHelper.greaterThan(since),
707
+ },
708
+ select: {
709
+ _id: true,
710
+ createdAt: true,
711
+ },
712
+ limit: MAX_CLUSTER_ROUND_ROWS,
713
+ skip: 0,
714
+ props: { isRoot: true },
715
+ });
576
716
 
577
- if (
578
- deadline === null ||
579
- deadline > now - PENDING_VERIFICATION_HOLD_GRACE_MINUTES * 60 * 1000
580
- ) {
581
- return {
582
- suggestionId: rowId,
583
- ruleNameSnapshot: row.ruleNameSnapshot,
584
- description: "applied a fix that is still being verified",
585
- isSameSubject: sameSubject,
586
- };
587
- }
588
- return null;
589
- }
717
+ const settledResourceRounds: number = (
718
+ await AutoRemediationSuggestionService.countBy({
719
+ query: {
720
+ ...(data.projectId ? { projectId: data.projectId } : {}),
721
+ resourceType: data.resourceType,
722
+ resourceId,
723
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
724
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
725
+ createdAt: QueryHelper.greaterThan(since),
726
+ },
727
+ props: { isRoot: true },
728
+ })
729
+ ).toNumber();
590
730
 
591
- if (row.status !== AutoRemediationSuggestionStatus.Planning) {
592
- return null;
593
- }
731
+ const inlineResourceJobs: Array<RunnerJob> = await RunnerJobService.findBy({
732
+ query: {
733
+ ...(data.projectId ? { projectId: data.projectId } : {}),
734
+ resourceType: data.resourceType,
735
+ resourceId,
736
+ origin: RunnerJobOrigin.AiRemediation,
737
+ stepId: QueryHelper.startsWith(INLINE_COMMAND_STEP_ID_PREFIX),
738
+ createdAt: QueryHelper.greaterThan(since),
739
+ },
740
+ select: {
741
+ _id: true,
742
+ autoRemediationSuggestionId: true,
743
+ },
744
+ limit: MAX_CLUSTER_ROUND_ROWS,
745
+ skip: 0,
746
+ props: { isRoot: true },
747
+ });
594
748
 
595
- const isRecent: boolean = row.createdAt
596
- ? new Date(row.createdAt).getTime() > inFlightSince
597
- : true;
749
+ const unattendedRuns: Set<string> = new Set<string>();
598
750
 
599
- if (!isRecent) {
600
- return null;
751
+ for (const job of inlineResourceJobs) {
752
+ unattendedRuns.add(
753
+ job.autoRemediationSuggestionId?.toString() || job.id?.toString() || "",
754
+ );
601
755
  }
602
756
 
603
- if (data.hasChangedCluster) {
604
- return {
605
- suggestionId: rowId,
606
- ruleNameSnapshot: row.ruleNameSnapshot,
607
- description: "is still changing it",
608
- isSameSubject: sameSubject,
609
- };
757
+ for (const round of inFlightRounds) {
758
+ if (isRoundOrderedBefore(round, data.forRound)) {
759
+ unattendedRuns.add(round.id?.toString() || "");
760
+ }
610
761
  }
611
762
 
612
- if (
613
- row.executionMode === AutoRemediationExecutionMode.FullAuto &&
614
- (data.anyOrder || isRoundOrderedBefore(row, data.forRound))
615
- ) {
616
- return {
617
- suggestionId: rowId,
618
- ruleNameSnapshot: row.ruleNameSnapshot,
619
- description: "is still running unattended",
620
- isSameSubject: sameSubject,
621
- };
763
+ // The asking round never counts against itself.
764
+ if (selfId) {
765
+ unattendedRuns.delete(selfId);
622
766
  }
623
767
 
624
- if (sameSubject) {
625
- return {
626
- suggestionId: rowId,
627
- ruleNameSnapshot: row.ruleNameSnapshot,
628
- description: "is still working on this same signal",
629
- isSameSubject: true,
630
- };
631
- }
768
+ const autoExecutedInWindow: number = Math.max(
769
+ settledResourceRounds,
770
+ unattendedRuns.size,
771
+ );
632
772
 
633
- return null;
773
+ return {
774
+ autoExecutedInWindow,
775
+ hasHeadroom: autoExecutedInWindow < MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR,
776
+ };
634
777
  }
635
778
 
636
779
  /*
637
- * Reload an incident and run the rules against it. Used by the
638
- * post-investigation remediation hand-off (RemediationHandoff), which
639
- * holds only the subject id: the create-hook's in-memory model is long
640
- * gone by the time the investigation settles, and the reload also picks
641
- * up edits made while the investigation ran. The select mirrors exactly
642
- * what doesIncidentMatchRule reads — keep the two in sync.
780
+ * Another AI run that holds this resource right now, or null — the
781
+ * resource sibling of findRoundHoldingCluster, with the same rules: a
782
+ * resource-level round holds its resource while it is still running
783
+ * unattended (only rounds created before the asking one, unless
784
+ * `anyOrder`) and while its executed fix is still being verified,
785
+ * whatever subject it is for; with `subject`, any round of that same
786
+ * signal still composing holds it too. A run that already changed the
787
+ * resource (an inline or approved job there, found through
788
+ * RunnerJob.resourceType/resourceId) holds it while it is still running
789
+ * and while its fix is being verified, whatever its order or mode.
790
+ *
791
+ * Throws on a failed read: callers fail safe to "held".
643
792
  */
644
793
  @CaptureSpan()
645
- public async applyRulesToIncidentById(data: {
646
- incidentId: ObjectID;
794
+ public async findRoundHoldingResource(data: {
647
795
  projectId: ObjectID;
648
- }): Promise<void> {
649
- const incident: Incident | null = await IncidentService.findOneById({
650
- id: data.incidentId,
651
- select: {
652
- _id: true,
653
- projectId: true,
654
- title: true,
655
- description: true,
656
- incidentSeverityId: true,
657
- monitors: { _id: true },
658
- labels: { _id: true },
659
- },
660
- props: { isRoot: true },
661
- });
796
+ resourceType: AiResourceType;
797
+ resourceId: string;
798
+ forRound?: ResourceRoundReference | undefined;
799
+ anyOrder?: boolean | undefined;
800
+ subject?: SubjectLinkage | undefined;
801
+ }): Promise<ResourceRoundHold | null> {
802
+ const resourceId: ObjectID = new ObjectID(data.resourceId);
662
803
 
663
- if (!incident) {
664
- return;
665
- }
804
+ const rows: Array<AutoRemediationSuggestion> =
805
+ await AutoRemediationSuggestionService.findBy({
806
+ query: {
807
+ projectId: data.projectId,
808
+ resourceType: data.resourceType,
809
+ resourceId,
810
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
811
+ status: QueryHelper.any([
812
+ AutoRemediationSuggestionStatus.Planning,
813
+ AutoRemediationSuggestionStatus.AutoExecuted,
814
+ AutoRemediationSuggestionStatus.Approved,
815
+ ]),
816
+ createdAt: QueryHelper.greaterThan(
817
+ OneUptimeDate.getSomeHoursAgo(HELD_ROUND_LOOKBACK_HOURS),
818
+ ),
819
+ },
820
+ select: {
821
+ _id: true,
822
+ status: true,
823
+ executionMode: true,
824
+ verificationStatus: true,
825
+ verificationDeadlineAt: true,
826
+ incidentId: true,
827
+ alertId: true,
828
+ ruleNameSnapshot: true,
829
+ createdAt: true,
830
+ },
831
+ limit: MAX_CLUSTER_ROUND_ROWS,
832
+ skip: 0,
833
+ props: { isRoot: true },
834
+ });
666
835
 
667
- await this.applyRulesToIncident(incident);
668
- }
836
+ const selfId: string | undefined = data.forRound?.suggestionId.toString();
669
837
 
670
- // The alert twin of applyRulesToIncidentById — mirrors doesAlertMatchRule.
671
- @CaptureSpan()
672
- public async applyRulesToAlertById(data: {
673
- alertId: ObjectID;
674
- projectId: ObjectID;
675
- }): Promise<void> {
676
- const alert: Alert | null = await AlertService.findOneById({
677
- id: data.alertId,
838
+ for (const row of rows) {
839
+ const rowId: string = row.id?.toString() || "";
840
+
841
+ if (!rowId || rowId === selfId) {
842
+ continue;
843
+ }
844
+
845
+ const hold: ResourceRoundHold | null = this.getHold({
846
+ row,
847
+ rowId,
848
+ hasChangedCluster: false,
849
+ anyOrder: data.anyOrder === true,
850
+ forRound: data.forRound,
851
+ subject: data.subject,
852
+ });
853
+
854
+ if (hold) {
855
+ return hold;
856
+ }
857
+ }
858
+
859
+ // Runs that changed this resource, found through their jobs on it.
860
+ const seen: Set<string> = new Set<string>(
861
+ rows.map((row: AutoRemediationSuggestion): string => {
862
+ return row.id?.toString() || "";
863
+ }),
864
+ );
865
+
866
+ if (selfId) {
867
+ seen.add(selfId);
868
+ }
869
+
870
+ const resourceJobs: Array<RunnerJob> = await RunnerJobService.findBy({
871
+ query: {
872
+ projectId: data.projectId,
873
+ resourceType: data.resourceType,
874
+ resourceId,
875
+ origin: RunnerJobOrigin.AiRemediation,
876
+ createdAt: QueryHelper.greaterThan(
877
+ OneUptimeDate.getSomeHoursAgo(HELD_ROUND_LOOKBACK_HOURS),
878
+ ),
879
+ },
880
+ select: {
881
+ _id: true,
882
+ autoRemediationSuggestionId: true,
883
+ stepId: true,
884
+ },
885
+ limit: MAX_CLUSTER_ROUND_ROWS,
886
+ skip: 0,
887
+ props: { isRoot: true },
888
+ });
889
+
890
+ const changedBy: Set<string> = new Set<string>();
891
+
892
+ for (const job of resourceJobs) {
893
+ const suggestionId: string =
894
+ job.autoRemediationSuggestionId?.toString() || "";
895
+ const stepId: string = job.stepId || "";
896
+
897
+ // A rollback job belongs to a fix whose verification already failed.
898
+ const isForwardChange: boolean =
899
+ stepId.startsWith(INLINE_COMMAND_STEP_ID_PREFIX) ||
900
+ stepId.startsWith(APPROVED_COMMAND_STEP_ID_PREFIX);
901
+
902
+ if (suggestionId && isForwardChange && !seen.has(suggestionId)) {
903
+ changedBy.add(suggestionId);
904
+ }
905
+ }
906
+
907
+ if (changedBy.size === 0) {
908
+ return null;
909
+ }
910
+
911
+ const runs: Array<AutoRemediationSuggestion> =
912
+ await AutoRemediationSuggestionService.findBy({
913
+ query: {
914
+ projectId: data.projectId,
915
+ _id: QueryHelper.any(Array.from(changedBy)),
916
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
917
+ status: QueryHelper.any([
918
+ AutoRemediationSuggestionStatus.Planning,
919
+ AutoRemediationSuggestionStatus.AutoExecuted,
920
+ AutoRemediationSuggestionStatus.Approved,
921
+ ]),
922
+ },
923
+ select: {
924
+ _id: true,
925
+ status: true,
926
+ executionMode: true,
927
+ verificationStatus: true,
928
+ verificationDeadlineAt: true,
929
+ incidentId: true,
930
+ alertId: true,
931
+ ruleNameSnapshot: true,
932
+ createdAt: true,
933
+ },
934
+ limit: MAX_CLUSTER_ROUND_ROWS,
935
+ skip: 0,
936
+ props: { isRoot: true },
937
+ });
938
+
939
+ for (const run of runs) {
940
+ const runId: string = run.id?.toString() || "";
941
+
942
+ if (!changedBy.has(runId)) {
943
+ continue;
944
+ }
945
+
946
+ const hold: ResourceRoundHold | null = this.getHold({
947
+ row: run,
948
+ rowId: runId,
949
+ hasChangedCluster: true,
950
+ anyOrder: data.anyOrder === true,
951
+ forRound: data.forRound,
952
+ subject: data.subject,
953
+ });
954
+
955
+ if (hold) {
956
+ return hold;
957
+ }
958
+ }
959
+
960
+ return null;
961
+ }
962
+
963
+ /*
964
+ * Whether one other run holds the cluster — see findRoundHoldingCluster.
965
+ * `hasChangedCluster`: the run already has a kubectl job on the cluster,
966
+ * so while it is still running it holds whatever its order or mode.
967
+ */
968
+ private getHold(data: {
969
+ row: AutoRemediationSuggestion;
970
+ rowId: string;
971
+ hasChangedCluster: boolean;
972
+ anyOrder: boolean;
973
+ forRound?: ClusterRoundReference | undefined;
974
+ subject?: SubjectLinkage | undefined;
975
+ }): ClusterRoundHold | null {
976
+ const { row, rowId } = data;
977
+ const now: number = OneUptimeDate.getCurrentDate().getTime();
978
+ const inFlightSince: number = OneUptimeDate.getSomeHoursAgo(
979
+ IN_FLIGHT_ROUND_WINDOW_HOURS,
980
+ ).getTime();
981
+
982
+ const sameSubject: boolean = data.subject
983
+ ? isSameSubject(row, data.subject)
984
+ : false;
985
+
986
+ if (
987
+ (row.status === AutoRemediationSuggestionStatus.AutoExecuted ||
988
+ row.status === AutoRemediationSuggestionStatus.Approved) &&
989
+ row.verificationStatus === AutoRemediationVerificationStatus.Pending
990
+ ) {
991
+ const deadline: number | null = row.verificationDeadlineAt
992
+ ? new Date(row.verificationDeadlineAt).getTime()
993
+ : null;
994
+
995
+ if (
996
+ deadline === null ||
997
+ deadline > now - PENDING_VERIFICATION_HOLD_GRACE_MINUTES * 60 * 1000
998
+ ) {
999
+ return {
1000
+ suggestionId: rowId,
1001
+ ruleNameSnapshot: row.ruleNameSnapshot,
1002
+ description: "applied a fix that is still being verified",
1003
+ isSameSubject: sameSubject,
1004
+ };
1005
+ }
1006
+ return null;
1007
+ }
1008
+
1009
+ if (row.status !== AutoRemediationSuggestionStatus.Planning) {
1010
+ return null;
1011
+ }
1012
+
1013
+ const isRecent: boolean = row.createdAt
1014
+ ? new Date(row.createdAt).getTime() > inFlightSince
1015
+ : true;
1016
+
1017
+ if (!isRecent) {
1018
+ return null;
1019
+ }
1020
+
1021
+ if (data.hasChangedCluster) {
1022
+ return {
1023
+ suggestionId: rowId,
1024
+ ruleNameSnapshot: row.ruleNameSnapshot,
1025
+ description: "is still changing it",
1026
+ isSameSubject: sameSubject,
1027
+ };
1028
+ }
1029
+
1030
+ if (
1031
+ row.executionMode === AutoRemediationExecutionMode.FullAuto &&
1032
+ (data.anyOrder || isRoundOrderedBefore(row, data.forRound))
1033
+ ) {
1034
+ return {
1035
+ suggestionId: rowId,
1036
+ ruleNameSnapshot: row.ruleNameSnapshot,
1037
+ description: "is still running unattended",
1038
+ isSameSubject: sameSubject,
1039
+ };
1040
+ }
1041
+
1042
+ if (sameSubject) {
1043
+ return {
1044
+ suggestionId: rowId,
1045
+ ruleNameSnapshot: row.ruleNameSnapshot,
1046
+ description: "is still working on this same signal",
1047
+ isSameSubject: true,
1048
+ };
1049
+ }
1050
+
1051
+ return null;
1052
+ }
1053
+
1054
+ /*
1055
+ * Reload an incident and run the rules against it. Used by the
1056
+ * post-investigation remediation hand-off (RemediationHandoff), which
1057
+ * holds only the subject id: the create-hook's in-memory model is long
1058
+ * gone by the time the investigation settles, and the reload also picks
1059
+ * up edits made while the investigation ran. The select mirrors exactly
1060
+ * what doesIncidentMatchRule reads — keep the two in sync.
1061
+ */
1062
+ @CaptureSpan()
1063
+ public async applyRulesToIncidentById(data: {
1064
+ incidentId: ObjectID;
1065
+ projectId: ObjectID;
1066
+ }): Promise<void> {
1067
+ const incident: Incident | null = await IncidentService.findOneById({
1068
+ id: data.incidentId,
1069
+ select: {
1070
+ _id: true,
1071
+ projectId: true,
1072
+ title: true,
1073
+ description: true,
1074
+ incidentSeverityId: true,
1075
+ monitors: { _id: true },
1076
+ labels: { _id: true },
1077
+ },
1078
+ props: { isRoot: true },
1079
+ });
1080
+
1081
+ if (!incident) {
1082
+ return;
1083
+ }
1084
+
1085
+ await this.applyRulesToIncident(incident);
1086
+ }
1087
+
1088
+ // The alert twin of applyRulesToIncidentById — mirrors doesAlertMatchRule.
1089
+ @CaptureSpan()
1090
+ public async applyRulesToAlertById(data: {
1091
+ alertId: ObjectID;
1092
+ projectId: ObjectID;
1093
+ }): Promise<void> {
1094
+ const alert: Alert | null = await AlertService.findOneById({
1095
+ id: data.alertId,
678
1096
  select: {
679
1097
  _id: true,
680
1098
  projectId: true,
@@ -779,6 +1197,8 @@ class AutoRemediationRuleEngineServiceClass {
779
1197
  _id: true,
780
1198
  autoRemediationRuleId: true,
781
1199
  kubernetesClusterId: true,
1200
+ resourceType: true,
1201
+ resourceId: true,
782
1202
  },
783
1203
  limit: MAX_SUGGESTIONS_PER_SUBJECT * 10,
784
1204
  skip: 0,
@@ -802,13 +1222,36 @@ class AutoRemediationRuleEngineServiceClass {
802
1222
  /*
803
1223
  * Cluster-level remediation first: an operator who set a mode on the
804
1224
  * cluster's AI page expressed a more specific intent than any project
805
- * rule, and it needs no rule to fire.
1225
+ * rule, and it needs no rule to fire. It starts nothing on a signal an
1226
+ * earlier pass already gave a resource round (one lane per signal).
1227
+ */
1228
+ const clusterRoundsStarted: number =
1229
+ await this.applyClusterLevelRemediation({
1230
+ projectId: data.projectId,
1231
+ linkage,
1232
+ existingSuggestions,
1233
+ budget: remainingBudget,
1234
+ });
1235
+
1236
+ remainingBudget -= clusterRoundsStarted;
1237
+
1238
+ if (remainingBudget <= 0) {
1239
+ return;
1240
+ }
1241
+
1242
+ /*
1243
+ * Resource-level remediation next, and only when the signal has no
1244
+ * cluster round: a resource's AI page expresses the same specific
1245
+ * intent a cluster's does, but one signal gets one AI fix lane — two
1246
+ * agents changing a cluster and a host for the same signal would each
1247
+ * verify and roll back on top of the other's change.
806
1248
  */
807
- remainingBudget -= await this.applyClusterLevelRemediation({
1249
+ remainingBudget -= await this.applyResourceLevelRemediation({
808
1250
  projectId: data.projectId,
809
1251
  linkage,
810
1252
  existingSuggestions,
811
1253
  budget: remainingBudget,
1254
+ clusterRoundStarted: clusterRoundsStarted > 0,
812
1255
  });
813
1256
 
814
1257
  if (remainingBudget <= 0) {
@@ -1133,6 +1576,12 @@ class AutoRemediationRuleEngineServiceClass {
1133
1576
  * cluster or being verified on it asks instead (see
1134
1577
  * startClusterCommandRun and findRoundHoldingCluster). Returns how much
1135
1578
  * of the per-subject budget it used.
1579
+ *
1580
+ * One AI fix lane per signal holds both ways: a signal that already has
1581
+ * a resource round (started by an earlier pass, while no linked cluster
1582
+ * was ready) gets no cluster round now — the two would each verify and
1583
+ * roll back on top of the other's change. A signal with no resource
1584
+ * round is unaffected.
1136
1585
  */
1137
1586
  private async applyClusterLevelRemediation(data: {
1138
1587
  projectId: ObjectID;
@@ -1142,6 +1591,10 @@ class AutoRemediationRuleEngineServiceClass {
1142
1591
  }): Promise<number> {
1143
1592
  let consumed: number = 0;
1144
1593
 
1594
+ if (hasResourceRound(data.existingSuggestions)) {
1595
+ return 0;
1596
+ }
1597
+
1145
1598
  let statuses: Array<KubernetesClusterAiAccessStatus> = [];
1146
1599
 
1147
1600
  try {
@@ -1257,6 +1710,34 @@ class AutoRemediationRuleEngineServiceClass {
1257
1710
  return false;
1258
1711
  }
1259
1712
 
1713
+ /*
1714
+ * One AI fix lane per signal: a signal whose resource lane holds it
1715
+ * (see applyClusterLevelRemediation) never gets another cluster
1716
+ * round from here either. A signal with no resource round is
1717
+ * unaffected.
1718
+ */
1719
+ const resourceRounds: Array<AutoRemediationSuggestion> =
1720
+ await AutoRemediationSuggestionService.findBy({
1721
+ query: {
1722
+ ...(linkage.incidentId
1723
+ ? { incidentId: linkage.incidentId }
1724
+ : { alertId: linkage.alertId! }),
1725
+ resourceId: QueryHelper.notNull(),
1726
+ },
1727
+ props: { isRoot: true },
1728
+ select: { _id: true, resourceId: true },
1729
+ limit: 1,
1730
+ skip: 0,
1731
+ });
1732
+
1733
+ if (hasResourceRound(resourceRounds)) {
1734
+ logger.debug(
1735
+ `AutoRemediationRuleEngine: this subject already has a resource AI round; not starting another round on cluster ${data.kubernetesClusterId.toString()}.`,
1736
+ { projectId: data.projectId.toString() } as LogAttributes,
1737
+ );
1738
+ return false;
1739
+ }
1740
+
1260
1741
  const status: KubernetesClusterAiAccessStatus | null =
1261
1742
  await KubernetesClusterAiAccessService.getStatusForCluster({
1262
1743
  clusterId: data.kubernetesClusterId,
@@ -1461,6 +1942,353 @@ class AutoRemediationRuleEngineServiceClass {
1461
1942
  return true;
1462
1943
  }
1463
1944
 
1945
+ /*
1946
+ * Resource-level remediation: the signal is linked to infrastructure
1947
+ * resources (Docker and Podman hosts, Docker Swarm, Proxmox, VMware and
1948
+ * Ceph clusters, database servers, hosts) and a resource's AI page lets AI
1949
+ * fix it. The first eligible resource — in ALL_AI_RESOURCE_TYPES order,
1950
+ * with fixes on (mode not Disabled) and remediation ready — gets ONE AI
1951
+ * command run with that resource as its only target, exactly as a
1952
+ * cluster's round does: Automatic runs its first round FullAuto (safe
1953
+ * changes, and allowlisted riskier ones, without a human; a riskier fix is
1954
+ * proposed), BypassApproval runs every round FullAuto (what the policy
1955
+ * says always needs a human still asks), RequireApproval runs Suggest.
1956
+ *
1957
+ * One resource lane per signal, and only when the signal has no cluster
1958
+ * round (started now or earlier): a cluster round is the more established
1959
+ * lane, and two AI rounds fixing different parts of one signal would each
1960
+ * verify and roll back on top of the other. Returns how much of the
1961
+ * per-subject budget it used (0 or 1). Never throws.
1962
+ */
1963
+ private async applyResourceLevelRemediation(data: {
1964
+ projectId: ObjectID;
1965
+ linkage: SubjectLinkage;
1966
+ existingSuggestions: Array<AutoRemediationSuggestion>;
1967
+ budget: number;
1968
+ clusterRoundStarted: boolean;
1969
+ }): Promise<number> {
1970
+ if (data.budget <= 0 || data.clusterRoundStarted) {
1971
+ return 0;
1972
+ }
1973
+
1974
+ const hasOtherAiRound: boolean = data.existingSuggestions.some(
1975
+ (suggestion: AutoRemediationSuggestion): boolean => {
1976
+ return Boolean(suggestion.kubernetesClusterId || suggestion.resourceId);
1977
+ },
1978
+ );
1979
+
1980
+ if (hasOtherAiRound) {
1981
+ return 0;
1982
+ }
1983
+
1984
+ let statuses: Array<ResourceAiAccessStatus> = [];
1985
+
1986
+ try {
1987
+ statuses = await ResourceAiAccessService.getStatusesForSubject({
1988
+ projectId: data.projectId,
1989
+ incidentId: data.linkage.incidentId,
1990
+ alertId: data.linkage.alertId,
1991
+ });
1992
+ } catch (error) {
1993
+ logger.error(
1994
+ `AutoRemediationRuleEngine: could not resolve infrastructure access for the subject; skipping resource-level remediation: ${error}`,
1995
+ { projectId: data.projectId.toString() } as LogAttributes,
1996
+ );
1997
+ return 0;
1998
+ }
1999
+
2000
+ const eligible: ResourceAiAccessStatus | undefined = statuses
2001
+ .filter((status: ResourceAiAccessStatus): boolean => {
2002
+ return (
2003
+ isAiResourceType(status.resourceType) &&
2004
+ status.aiRemediationMode !== ResourceAiRemediationMode.Disabled &&
2005
+ status.isRemediationReady
2006
+ );
2007
+ })
2008
+ .sort((a: ResourceAiAccessStatus, b: ResourceAiAccessStatus): number => {
2009
+ return (
2010
+ ALL_AI_RESOURCE_TYPES.indexOf(a.resourceType) -
2011
+ ALL_AI_RESOURCE_TYPES.indexOf(b.resourceType)
2012
+ );
2013
+ })[0];
2014
+
2015
+ if (!eligible) {
2016
+ return 0;
2017
+ }
2018
+
2019
+ try {
2020
+ const started: boolean = await this.startResourceCommandRun({
2021
+ projectId: data.projectId,
2022
+ resource: eligible,
2023
+ linkage: data.linkage,
2024
+ round: 1,
2025
+ });
2026
+
2027
+ return started ? 1 : 0;
2028
+ } catch (error) {
2029
+ logger.error(
2030
+ `AutoRemediationRuleEngine: could not start resource-level remediation for ${eligible.resourceType} ${eligible.resourceId}: ${error}`,
2031
+ { projectId: data.projectId.toString() } as LogAttributes,
2032
+ );
2033
+ return 0;
2034
+ }
2035
+ }
2036
+
2037
+ /*
2038
+ * A follow-up round after a resource plan ran and verification failed —
2039
+ * startFollowUpClusterRemediation for a resource: Suggest for
2040
+ * RequireApproval and Automatic resources, unattended again for
2041
+ * BypassApproval ones, capped by MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT,
2042
+ * and asking first whatever the mode when `forceSuggest` is set (the
2043
+ * failed round's rollback did not complete). Called by the verifier;
2044
+ * never throws.
2045
+ */
2046
+ @CaptureSpan()
2047
+ public async startFollowUpResourceRemediation(data: {
2048
+ projectId: ObjectID;
2049
+ resourceType: AiResourceType;
2050
+ resourceId: ObjectID;
2051
+ incidentId?: ObjectID | undefined;
2052
+ alertId?: ObjectID | undefined;
2053
+ forceSuggest?: boolean | undefined;
2054
+ // Completes "this round asks first because ..." on the feed.
2055
+ forceSuggestReason?: string | undefined;
2056
+ }): Promise<boolean> {
2057
+ try {
2058
+ if (!isAiResourceType(data.resourceType)) {
2059
+ return false;
2060
+ }
2061
+
2062
+ const project: Project | null = await ProjectService.findOneById({
2063
+ id: data.projectId,
2064
+ select: { enableAutoRemediation: true },
2065
+ props: { isRoot: true },
2066
+ });
2067
+
2068
+ if (!project || project.enableAutoRemediation === false) {
2069
+ return false;
2070
+ }
2071
+
2072
+ const linkage: SubjectLinkage = {
2073
+ incidentId: data.incidentId,
2074
+ alertId: data.alertId,
2075
+ };
2076
+
2077
+ if (!linkage.incidentId && !linkage.alertId) {
2078
+ return false;
2079
+ }
2080
+
2081
+ const priorRounds: number = (
2082
+ await AutoRemediationSuggestionService.countBy({
2083
+ query: {
2084
+ ...(linkage.incidentId
2085
+ ? { incidentId: linkage.incidentId }
2086
+ : { alertId: linkage.alertId! }),
2087
+ resourceType: data.resourceType,
2088
+ resourceId: data.resourceId,
2089
+ },
2090
+ props: { isRoot: true },
2091
+ })
2092
+ ).toNumber();
2093
+
2094
+ if (priorRounds >= MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT) {
2095
+ logger.debug(
2096
+ `AutoRemediationRuleEngine: ${data.resourceType} ${data.resourceId.toString()} already used ${priorRounds} remediation round(s) on this subject; not asking again.`,
2097
+ { projectId: data.projectId.toString() } as LogAttributes,
2098
+ );
2099
+ return false;
2100
+ }
2101
+
2102
+ const status: ResourceAiAccessStatus | null =
2103
+ await ResourceAiAccessService.getStatusForResource({
2104
+ projectId: data.projectId,
2105
+ resourceType: data.resourceType,
2106
+ resourceId: data.resourceId,
2107
+ });
2108
+
2109
+ if (
2110
+ !status ||
2111
+ !status.isRemediationReady ||
2112
+ status.aiRemediationMode === ResourceAiRemediationMode.Disabled
2113
+ ) {
2114
+ return false;
2115
+ }
2116
+
2117
+ return await this.startResourceCommandRun({
2118
+ projectId: data.projectId,
2119
+ resource: status,
2120
+ linkage,
2121
+ round: priorRounds + 1,
2122
+ askFirstReason:
2123
+ data.forceSuggest === true
2124
+ ? data.forceSuggestReason ||
2125
+ "the previous fix's rollback did not complete, so its change may still be applied"
2126
+ : undefined,
2127
+ });
2128
+ } catch (error) {
2129
+ logger.error(
2130
+ `AutoRemediationRuleEngine: follow-up resource remediation failed: ${error}`,
2131
+ { projectId: data.projectId.toString() } as LogAttributes,
2132
+ );
2133
+ return false;
2134
+ }
2135
+ }
2136
+
2137
+ /*
2138
+ * startClusterCommandRun for a resource: the suggestion carries the
2139
+ * resource's type and id, its snapshot mode follows the same rules
2140
+ * (unattended for BypassApproval, and for Automatic on round 1 only;
2141
+ * asking first when another round holds the resource or that cannot be
2142
+ * checked), and the feed says exactly what the round does.
2143
+ */
2144
+ private async startResourceCommandRun(data: {
2145
+ projectId: ObjectID;
2146
+ resource: ResourceAiAccessStatus;
2147
+ linkage: SubjectLinkage;
2148
+ round: number;
2149
+ // Set when a round that would run unattended must ask first anyway.
2150
+ askFirstReason?: string | undefined;
2151
+ }): Promise<boolean> {
2152
+ const { resource } = data;
2153
+ const noun: string = describeResourceNoun(resource.resourceType);
2154
+ const label: string = describeResourceForFeed(resource);
2155
+ const agentName: string =
2156
+ AI_RESOURCE_TYPE_INFO[resource.resourceType].agentDisplayName;
2157
+
2158
+ const isBypass: boolean =
2159
+ resource.aiRemediationMode === ResourceAiRemediationMode.BypassApproval;
2160
+ const wantsUnattended: boolean = doesResourceModeRunRoundUnattended(
2161
+ resource.aiRemediationMode,
2162
+ data.round,
2163
+ );
2164
+
2165
+ let askFirstReason: string | null = data.askFirstReason || null;
2166
+
2167
+ // One unattended round per resource at a time (see startClusterCommandRun).
2168
+ if (wantsUnattended && !askFirstReason) {
2169
+ try {
2170
+ const hold: ResourceRoundHold | null =
2171
+ await this.findRoundHoldingResource({
2172
+ projectId: data.projectId,
2173
+ resourceType: resource.resourceType,
2174
+ resourceId: resource.resourceId,
2175
+ anyOrder: true,
2176
+ });
2177
+
2178
+ if (hold) {
2179
+ askFirstReason = `another OneUptime AI round on this ${noun} ${hold.description}`;
2180
+ }
2181
+ } catch (error) {
2182
+ logger.error(
2183
+ `AutoRemediationRuleEngine: could not check ${resource.resourceType} ${resource.resourceId} for another AI round in flight; this round asks first: ${error}`,
2184
+ { projectId: data.projectId.toString() } as LogAttributes,
2185
+ );
2186
+ askFirstReason = `OneUptime AI could not confirm that no other AI round is changing this ${noun}`;
2187
+ }
2188
+ }
2189
+
2190
+ const isAutomatic: boolean = wantsUnattended && !askFirstReason;
2191
+
2192
+ const suggestion: AutoRemediationSuggestion =
2193
+ new AutoRemediationSuggestion();
2194
+ suggestion.projectId = data.projectId;
2195
+ suggestion.resourceType = resource.resourceType;
2196
+ suggestion.resourceId = new ObjectID(resource.resourceId);
2197
+ suggestion.ruleNameSnapshot = getResourceRoundNameSnapshot(
2198
+ resource.resourceType,
2199
+ resource.resourceName,
2200
+ data.round,
2201
+ );
2202
+ suggestion.status = AutoRemediationSuggestionStatus.Planning;
2203
+ suggestion.suggestionType = AutoRemediationSuggestionType.CommandPlan;
2204
+ suggestion.executionMode = isAutomatic
2205
+ ? AutoRemediationExecutionMode.FullAuto
2206
+ : AutoRemediationExecutionMode.Suggest;
2207
+ suggestion.verificationWindowMinutes = DEFAULT_VERIFICATION_WINDOW_MINUTES;
2208
+ // Closed loop only when nobody approves: the human resolves otherwise.
2209
+ suggestion.autoResolveOnRecovery = isAutomatic;
2210
+ if (data.linkage.incidentId) {
2211
+ suggestion.incidentId = data.linkage.incidentId;
2212
+ }
2213
+ if (data.linkage.alertId) {
2214
+ suggestion.alertId = data.linkage.alertId;
2215
+ }
2216
+
2217
+ const created: AutoRemediationSuggestion =
2218
+ await AutoRemediationSuggestionService.create({
2219
+ data: suggestion,
2220
+ props: { isRoot: true },
2221
+ });
2222
+
2223
+ const aiRunId: ObjectID | null = await AIInvestigationQueue.enqueue({
2224
+ projectId: data.projectId,
2225
+ subjectIncidentId: data.linkage.incidentId,
2226
+ subjectAlertId: data.linkage.alertId,
2227
+ subjectAutoRemediationSuggestionId: created.id!,
2228
+ remediationRunType: AIRunType.RemediationExecution,
2229
+ });
2230
+
2231
+ if (!aiRunId) {
2232
+ await AutoRemediationSuggestionService.updateOneById({
2233
+ id: created.id!,
2234
+ data: {
2235
+ status: AutoRemediationSuggestionStatus.NoneApplicable,
2236
+ rationaleMarkdown:
2237
+ "The AI remediation run could not be started — the daily autonomous AI budget is exhausted or no run could be queued. Re-enable by raising the budget or waiting for the daily reset.",
2238
+ },
2239
+ props: { isRoot: true },
2240
+ });
2241
+ return false;
2242
+ }
2243
+
2244
+ await AutoRemediationSuggestionService.updateOneById({
2245
+ id: created.id!,
2246
+ data: { aiRunId },
2247
+ props: { isRoot: true },
2248
+ });
2249
+
2250
+ /*
2251
+ * The feed states exactly what this round does, in the terms of the
2252
+ * canonical ResourceAiRemediationMode comment — the resource wording of
2253
+ * startClusterCommandRun's announcement.
2254
+ */
2255
+ const proposalSentence: string = getResourceRoundProposalFeedSentence(noun);
2256
+ let markdown: string;
2257
+
2258
+ if (wantsUnattended && askFirstReason) {
2259
+ markdown = `⚡ **OneUptime AI is composing ${
2260
+ data.round > 1 ? "another" : "a"
2261
+ } fix for ${label}**${
2262
+ data.round > 1
2263
+ ? ` (round ${data.round}) — the previous fix did not recover the service`
2264
+ : ""
2265
+ }. This round asks first because ${askFirstReason}: nothing runs until you approve the plan, which will appear here shortly.`;
2266
+ } else if (isBypass) {
2267
+ markdown =
2268
+ data.round > 1
2269
+ ? `⚡ **OneUptime AI is applying another fix on ${label}** (round ${data.round}) — the previous fix did not recover the service. Approvals are bypassed for this ${noun}, so the new fix runs on its own through its ${agentName}: every change the command policy allows, safe or riskier — except that ${RESOURCE_ALWAYS_ASKS_SUMMARY}, so AI proposes such a change for your approval; destructive commands never run. ${proposalSentence} Progress appears here.`
2270
+ : `⚡ **OneUptime AI is fixing ${label}.** Approvals are bypassed for this ${noun}: AI is diagnosing through its ${agentName} and will apply whatever fix the command policy allows — safe or riskier — on its own, without asking, except that ${RESOURCE_ALWAYS_ASKS_SUMMARY}, so AI proposes such a change for your approval; destructive commands never run. ${proposalSentence} Progress appears here.`;
2271
+ } else if (isAutomatic) {
2272
+ markdown = `⚡ **OneUptime AI is fixing ${label}.** Automatic remediation is on for this ${noun}: AI is diagnosing through its ${agentName} and will apply safe changes (${RESOURCE_SAFE_CHANGES_SUMMARY}), plus riskier changes whose shape the ${noun}'s command allowlist names, on its own. A riskier change never runs on its own otherwise: if the round finds only riskier fixes, AI proposes exactly those for your one-click approval; if it also applied safe changes, a riskier fix is proposed only if verification shows the service did not recover. ${capitalizeFirst(
2273
+ RESOURCE_ALWAYS_ASKS_SUMMARY,
2274
+ )}, and destructive commands never run. ${proposalSentence} Progress appears here.`;
2275
+ } else {
2276
+ markdown =
2277
+ data.round > 1
2278
+ ? `⚡ **OneUptime AI is composing another fix for ${label}** (round ${data.round}) — the previous fix did not recover the service. This round asks first: nothing runs until you approve the new plan, which will appear here shortly.`
2279
+ : `⚡ **OneUptime AI is composing a fix for ${label}.** Nothing runs until you approve the plan — it will appear here shortly.`;
2280
+ }
2281
+
2282
+ await this.postFeedItem({
2283
+ projectId: data.projectId,
2284
+ linkage: data.linkage,
2285
+ markdown,
2286
+ pingWorkspace: false,
2287
+ });
2288
+
2289
+ return true;
2290
+ }
2291
+
1464
2292
  /*
1465
2293
  * AI path: create a Planning suggestion and enqueue a RemediationPlan run
1466
2294
  * on the durable AI queue. The planning runner fills in the picked runbook