@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,1249 @@
1
+ import RemediationExecutionRunner, {
2
+ ResourceBreakerState,
3
+ ResourceModeResolution,
4
+ isClusterRemediationRound,
5
+ isResourceRemediationRound,
6
+ } from "../../../../Server/Utils/AI/Remediation/RemediationExecutionRunner";
7
+ import AIInvestigationEngine, {
8
+ InvestigationRequest,
9
+ } from "../../../../Server/Utils/AI/SRE/AIInvestigationEngine";
10
+ import { ConfidenceSignal } from "../../../../Server/Utils/AI/SRE/ConfidenceSignal";
11
+ import {
12
+ ObservabilityAssistantExtraTool,
13
+ ObservabilityAssistantResult,
14
+ } from "../../../../Server/Utils/AI/Chat/ObservabilityAssistant";
15
+ import { ToolCallOutcome } from "../../../../Server/Utils/AI/Toolbox/Index";
16
+ import AIRunService from "../../../../Server/Services/AIRunService";
17
+ import AlertFeedService from "../../../../Server/Services/AlertFeedService";
18
+ import AutoRemediationRuleService from "../../../../Server/Services/AutoRemediationRuleService";
19
+ import AutoRemediationSuggestionService from "../../../../Server/Services/AutoRemediationSuggestionService";
20
+ import IncidentFeedService from "../../../../Server/Services/IncidentFeedService";
21
+ import IncidentService from "../../../../Server/Services/IncidentService";
22
+ import KubernetesClusterAiAccessService from "../../../../Server/Services/KubernetesClusterAiAccessService";
23
+ import ProjectService from "../../../../Server/Services/ProjectService";
24
+ import ResourceAiAccessService from "../../../../Server/Services/ResourceAiAccessService";
25
+ import RunnerJobService from "../../../../Server/Services/RunnerJobService";
26
+ import { MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR } from "../../../../Server/Services/AutoRemediationRuleEngineService";
27
+ import PostedRootCause from "../../../../Server/Utils/AI/SRE/PostedRootCause";
28
+ import logger from "../../../../Server/Utils/Logger";
29
+ import Semaphore, {
30
+ SemaphoreMutex,
31
+ } from "../../../../Server/Infrastructure/Semaphore";
32
+ import AutoRemediationSuggestion from "../../../../Models/DatabaseModels/AutoRemediationSuggestion";
33
+ import Incident from "../../../../Models/DatabaseModels/Incident";
34
+ import Project from "../../../../Models/DatabaseModels/Project";
35
+ import RunnerJob from "../../../../Models/DatabaseModels/RunnerJob";
36
+ import AutoRemediationExecutionMode from "../../../../Types/AutoRemediation/AutoRemediationExecutionMode";
37
+ import AutoRemediationSuggestionStatus from "../../../../Types/AutoRemediation/AutoRemediationSuggestionStatus";
38
+ import AutoRemediationSuggestionType from "../../../../Types/AutoRemediation/AutoRemediationSuggestionType";
39
+ import AutoRemediationVerificationStatus from "../../../../Types/AutoRemediation/AutoRemediationVerificationStatus";
40
+ import {
41
+ AiRemediationCommandExecutionStatus,
42
+ AiRemediationCommandPlan,
43
+ AiRemediationCommandPlanUtil,
44
+ AiRemediationCommandPolicyVerdict,
45
+ AiRemediationRollbackStatus,
46
+ RESOURCE_UNATTENDED_ROUND_BECOMES_PROPOSAL_SUMMARY,
47
+ } from "../../../../Types/AutoRemediation/AiRemediationCommandPlan";
48
+ import AiResourceType from "../../../../Types/ResourceAiAgent/AiResourceType";
49
+ import {
50
+ ResourceAiAccessStatus,
51
+ ResourceAiAgentPosture,
52
+ ResourceAiRemediationMode,
53
+ ResourceCommandTier,
54
+ } from "../../../../Types/ResourceAiAgent/ResourceAiAccess";
55
+ import RunbookStepType from "../../../../Types/Runbook/RunbookStepType";
56
+ import RunnerJobStatus from "../../../../Types/Runbook/RunnerJobStatus";
57
+ import {
58
+ LIST_INFRASTRUCTURE_ACCESS_TOOL_NAME,
59
+ RUN_INFRASTRUCTURE_COMMAND_TOOL_NAME,
60
+ } from "../../../../Server/Utils/AI/ResourceAccess/ResourceAccessToolNames";
61
+ import { JSONObject } from "../../../../Types/JSON";
62
+ import ObjectID from "../../../../Types/ObjectID";
63
+ import PositiveNumber from "../../../../Types/PositiveNumber";
64
+ import OneUptimeDate from "../../../../Types/Date";
65
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
66
+
67
+ /*
68
+ * Contract under test — a RESOURCE round of RemediationExecutionRunner
69
+ * (remediation a resource's AI agent page asked for: a Docker or Podman
70
+ * host, a Docker Swarm, Proxmox, VMware or Ceph cluster, a database server,
71
+ * a host), built beside the cluster round and mirroring it:
72
+ *
73
+ * - the round is read off the suggestion row (a resource, no rule); its
74
+ * consent is the resource's own, so the project's AI command execution
75
+ * opt-in does not gate it, while Enable AI / auto-remediation still do;
76
+ * - the resource is re-read at the start and must be remediation-ready; a
77
+ * deleted or no-longer-ready resource settles NoneApplicable with why;
78
+ * - the mode is resolved exactly like a cluster's: a FullAuto snapshot runs
79
+ * unattended only while the resource's mode still is unattended, its
80
+ * hourly breaker has headroom and no other round holds it — each
81
+ * downgrade corrects the row, posts to the feed and opens the rationale;
82
+ * - the model gets the resource toolkit (ResourceCommand on this resource
83
+ * only) plus the read-only infrastructure tool, a persona and a question
84
+ * that name the resource and its programs, and a context block for it;
85
+ * - an unattended round that ran nothing but kept changes for a human
86
+ * settles Suggested with exactly those — after re-checking the resource
87
+ * live (ready, same agent, the agent's write scope) — else NoneApplicable;
88
+ * - earlier rounds on the same resource for the same signal are shown.
89
+ */
90
+
91
+ const PROJECT_ID: ObjectID = new ObjectID(
92
+ "22222222-2222-4222-8222-222222222222",
93
+ );
94
+ const RUN_ID: ObjectID = new ObjectID("88888888-8888-4888-8888-888888888888");
95
+ const SUGGESTION_ID: ObjectID = new ObjectID(
96
+ "77777777-7777-4777-8777-777777777777",
97
+ );
98
+ const OTHER_SUGGESTION_ID: ObjectID = new ObjectID(
99
+ "79797979-7979-4979-8979-797979797979",
100
+ );
101
+ const INCIDENT_ID: ObjectID = new ObjectID(
102
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
103
+ );
104
+ const RESOURCE_ID: ObjectID = new ObjectID(
105
+ "33333333-3333-4333-8333-333333333333",
106
+ );
107
+ const AGENT_ID: string = "44444444-4444-4444-8444-444444444444";
108
+ const OTHER_AGENT_ID: string = "45454545-4545-4545-8545-454545454545";
109
+ const JOB_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
110
+
111
+ function posture(
112
+ overrides: Partial<ResourceAiAgentPosture> = {},
113
+ ): ResourceAiAgentPosture {
114
+ return {
115
+ resourceType: AiResourceType.DockerHost,
116
+ resourceIdentifier: "web-1",
117
+ allowWrites: true,
118
+ writeTargets: [],
119
+ protectedTargets: [],
120
+ reachable: true,
121
+ ...overrides,
122
+ };
123
+ }
124
+
125
+ function resource(
126
+ overrides: Partial<ResourceAiAccessStatus> = {},
127
+ postureOverrides: Partial<ResourceAiAgentPosture> = {},
128
+ ): ResourceAiAccessStatus {
129
+ return {
130
+ resourceType: AiResourceType.DockerHost,
131
+ resourceId: RESOURCE_ID.toString(),
132
+ resourceName: "web-1",
133
+ isAiInvestigationEnabled: true,
134
+ aiRemediationMode: ResourceAiRemediationMode.Automatic,
135
+ aiCommandAllowlist: [],
136
+ agent: {
137
+ agentId: AGENT_ID,
138
+ connectionStatus: "connected",
139
+ isOnline: true,
140
+ posture: posture(postureOverrides),
141
+ },
142
+ gaps: [],
143
+ isInvestigationReady: true,
144
+ isRemediationReady: true,
145
+ ...overrides,
146
+ };
147
+ }
148
+
149
+ // The suggestion row as the database holds it: a resource round.
150
+ function resourceRow(
151
+ overrides: Record<string, unknown> = {},
152
+ ): Record<string, unknown> {
153
+ return {
154
+ id: SUGGESTION_ID,
155
+ _id: SUGGESTION_ID.toString(),
156
+ projectId: PROJECT_ID,
157
+ status: AutoRemediationSuggestionStatus.Planning,
158
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
159
+ executionMode: AutoRemediationExecutionMode.FullAuto,
160
+ autoResolveOnRecovery: true,
161
+ incidentId: INCIDENT_ID,
162
+ resourceType: AiResourceType.DockerHost,
163
+ resourceId: RESOURCE_ID,
164
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1"',
165
+ verificationWindowMinutes: 15,
166
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
167
+ ...overrides,
168
+ };
169
+ }
170
+
171
+ function mockSuggestionHonouringSelect(
172
+ row: Record<string, unknown>,
173
+ ): jest.SpyInstance {
174
+ return jest
175
+ .spyOn(AutoRemediationSuggestionService, "findOneById")
176
+ .mockImplementation(
177
+ async (args: unknown): Promise<AutoRemediationSuggestion> => {
178
+ const select: Record<string, unknown> = (
179
+ args as { select: Record<string, unknown> }
180
+ ).select;
181
+ const picked: Record<string, unknown> = { id: row["id"] };
182
+ for (const key of Object.keys(select)) {
183
+ if (key in row) {
184
+ picked[key] = row[key];
185
+ }
186
+ }
187
+ return picked as unknown as AutoRemediationSuggestion;
188
+ },
189
+ );
190
+ }
191
+
192
+ function captureRequest(): { get: () => InvestigationRequest } {
193
+ const captured: { value: InvestigationRequest | null } = { value: null };
194
+
195
+ jest
196
+ .spyOn(AIInvestigationEngine, "executeRun")
197
+ .mockImplementation(
198
+ async (data: { request: InvestigationRequest }): Promise<void> => {
199
+ captured.value = data.request;
200
+ },
201
+ );
202
+
203
+ return {
204
+ get: (): InvestigationRequest => {
205
+ expect(captured.value).not.toBeNull();
206
+ return captured.value!;
207
+ },
208
+ };
209
+ }
210
+
211
+ function toolNames(request: InvestigationRequest): Array<string> {
212
+ return (request.extraTools || []).map(
213
+ (tool: ObservabilityAssistantExtraTool) => {
214
+ return tool.definition.name;
215
+ },
216
+ );
217
+ }
218
+
219
+ function findTool(
220
+ request: InvestigationRequest,
221
+ name: string,
222
+ ): ObservabilityAssistantExtraTool {
223
+ const tool: ObservabilityAssistantExtraTool | undefined = (
224
+ request.extraTools || []
225
+ ).find((candidate: ObservabilityAssistantExtraTool) => {
226
+ return candidate.definition.name === name;
227
+ });
228
+ expect(tool).toBeDefined();
229
+ return tool!;
230
+ }
231
+
232
+ function postAnalysisArgs(analysisMarkdown: string): {
233
+ analysisMarkdown: string;
234
+ confidence: ConfidenceSignal;
235
+ result: ObservabilityAssistantResult;
236
+ } {
237
+ return {
238
+ analysisMarkdown,
239
+ confidence: {
240
+ confident: true,
241
+ source: "classification",
242
+ } as ConfidenceSignal,
243
+ result: {} as ObservabilityAssistantResult,
244
+ };
245
+ }
246
+
247
+ async function run(): Promise<void> {
248
+ await RemediationExecutionRunner.executeRemediation({
249
+ aiRunId: RUN_ID,
250
+ projectId: PROJECT_ID,
251
+ suggestionId: SUGGESTION_ID,
252
+ attemptCount: 1,
253
+ });
254
+ }
255
+
256
+ function resourceArgs(
257
+ overrides: Partial<Record<string, unknown>> = {},
258
+ ): JSONObject {
259
+ return {
260
+ stepType: "ResourceCommand",
261
+ resourceId: RESOURCE_ID.toString(),
262
+ command: "docker restart web",
263
+ rationale: "the web container is wedged",
264
+ expectedEffect: "web serves again",
265
+ ...overrides,
266
+ } as JSONObject;
267
+ }
268
+
269
+ describe("resource round classification", () => {
270
+ it("a suggestion that names a resource and no rule is a resource round, never a cluster one", () => {
271
+ expect(
272
+ isResourceRemediationRound({
273
+ resourceType: AiResourceType.Host,
274
+ resourceId: RESOURCE_ID,
275
+ }),
276
+ ).toBe(true);
277
+ expect(
278
+ isClusterRemediationRound({
279
+ resourceType: AiResourceType.Host,
280
+ resourceId: RESOURCE_ID,
281
+ } as never),
282
+ ).toBe(false);
283
+ });
284
+
285
+ it.each([
286
+ [
287
+ "a rule round",
288
+ {
289
+ resourceType: AiResourceType.Host,
290
+ resourceId: RESOURCE_ID,
291
+ autoRemediationRuleId: ObjectID.generate(),
292
+ },
293
+ ],
294
+ ["a row without a type", { resourceId: RESOURCE_ID }],
295
+ ["a row without an id", { resourceType: AiResourceType.Host }],
296
+ ["a cluster round", { kubernetesClusterId: ObjectID.generate() }],
297
+ ])(
298
+ "%s is not a resource round",
299
+ (_label: string, row: Record<string, unknown>) => {
300
+ expect(isResourceRemediationRound(row as never)).toBe(false);
301
+ },
302
+ );
303
+ });
304
+
305
+ describe("RemediationExecutionRunner.checkProjectGates for a resource round", () => {
306
+ afterEach(() => {
307
+ jest.restoreAllMocks();
308
+ });
309
+
310
+ it("does not need the AI command execution opt-in, but still needs AI and auto-remediation", async () => {
311
+ const project: jest.SpyInstance = jest
312
+ .spyOn(ProjectService, "findOneById")
313
+ .mockResolvedValue({
314
+ enableAi: true,
315
+ enableAutoRemediation: true,
316
+ enableAiCommandExecution: false,
317
+ } as unknown as Project);
318
+
319
+ expect(
320
+ await RemediationExecutionRunner.checkProjectGates({
321
+ projectId: PROJECT_ID,
322
+ isClusterRound: false,
323
+ isResourceRound: true,
324
+ }),
325
+ ).toBeNull();
326
+
327
+ // A rule round in the same project is still stopped by the opt-in.
328
+ expect(
329
+ await RemediationExecutionRunner.checkProjectGates({
330
+ projectId: PROJECT_ID,
331
+ isClusterRound: false,
332
+ }),
333
+ ).toContain("AI command execution is not enabled");
334
+
335
+ project.mockResolvedValue({
336
+ enableAi: false,
337
+ enableAutoRemediation: true,
338
+ } as unknown as Project);
339
+
340
+ expect(
341
+ await RemediationExecutionRunner.checkProjectGates({
342
+ projectId: PROJECT_ID,
343
+ isClusterRound: false,
344
+ isResourceRound: true,
345
+ }),
346
+ ).toContain("AI or auto-remediation was disabled");
347
+ });
348
+ });
349
+
350
+ describe("RemediationExecutionRunner.resolveResourceMode", () => {
351
+ let countBy: jest.SpyInstance;
352
+ let jobFindBy: jest.SpyInstance;
353
+ let suggestionFindBy: jest.SpyInstance;
354
+
355
+ function suggestion(
356
+ executionMode: AutoRemediationExecutionMode,
357
+ ): AutoRemediationSuggestion {
358
+ return {
359
+ id: SUGGESTION_ID,
360
+ projectId: PROJECT_ID,
361
+ resourceType: AiResourceType.DockerHost,
362
+ resourceId: RESOURCE_ID,
363
+ executionMode,
364
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
365
+ } as unknown as AutoRemediationSuggestion;
366
+ }
367
+
368
+ beforeEach(() => {
369
+ jest.spyOn(logger, "warn").mockImplementation((): void => {
370
+ return undefined;
371
+ });
372
+ jest.spyOn(logger, "error").mockImplementation((): void => {
373
+ return undefined;
374
+ });
375
+ countBy = jest
376
+ .spyOn(AutoRemediationSuggestionService, "countBy")
377
+ .mockResolvedValue(new PositiveNumber(0));
378
+ jobFindBy = jest.spyOn(RunnerJobService, "findBy").mockResolvedValue([]);
379
+ suggestionFindBy = jest
380
+ .spyOn(AutoRemediationSuggestionService, "findBy")
381
+ .mockResolvedValue([]);
382
+ });
383
+
384
+ afterEach(() => {
385
+ jest.restoreAllMocks();
386
+ });
387
+
388
+ it("a Suggest snapshot asks, without reading the breaker", async () => {
389
+ const resolution: ResourceModeResolution =
390
+ await RemediationExecutionRunner.resolveResourceMode({
391
+ suggestion: suggestion(AutoRemediationExecutionMode.Suggest),
392
+ resource: resource({
393
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
394
+ }),
395
+ });
396
+
397
+ expect(resolution.mode).toBe("Suggest");
398
+ expect(resolution.downgradedByModeChange).toBe(false);
399
+ expect(countBy).not.toHaveBeenCalled();
400
+ });
401
+
402
+ it.each([
403
+ ResourceAiRemediationMode.Automatic,
404
+ ResourceAiRemediationMode.BypassApproval,
405
+ ])(
406
+ "a FullAuto snapshot on a %s resource with headroom and no hold runs FullAuto",
407
+ async (mode: ResourceAiRemediationMode) => {
408
+ const resolution: ResourceModeResolution =
409
+ await RemediationExecutionRunner.resolveResourceMode({
410
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
411
+ resource: resource({ aiRemediationMode: mode }),
412
+ });
413
+
414
+ expect(resolution).toMatchObject({
415
+ mode: "FullAuto",
416
+ downgradedByCircuitBreaker: false,
417
+ downgradedByModeChange: false,
418
+ downgradedByInFlightRound: false,
419
+ autoExecutedInWindow: 0,
420
+ });
421
+ // The breaker and the hold read the resource, never a cluster.
422
+ expect(
423
+ (countBy.mock.calls[0]![0] as { query: Record<string, unknown> }).query,
424
+ ).toMatchObject({
425
+ resourceType: AiResourceType.DockerHost,
426
+ resourceId: RESOURCE_ID,
427
+ });
428
+ expect(jobFindBy).toHaveBeenCalledTimes(2);
429
+ },
430
+ );
431
+
432
+ it("downgrades a FullAuto snapshot when the operator moved the resource to ask for approval", async () => {
433
+ const resolution: ResourceModeResolution =
434
+ await RemediationExecutionRunner.resolveResourceMode({
435
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
436
+ resource: resource({
437
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
438
+ }),
439
+ });
440
+
441
+ expect(resolution.mode).toBe("Suggest");
442
+ expect(resolution.downgradedByModeChange).toBe(true);
443
+ expect(countBy).not.toHaveBeenCalled();
444
+ });
445
+
446
+ /*
447
+ * Automatic runs only a signal's FIRST round unattended. A follow-up
448
+ * round started FullAuto under Bypass approval must ask once the
449
+ * resource is on Automatic — the round is read off the round's
450
+ * server-written name.
451
+ */
452
+ it("downgrades a FullAuto follow-up round when the operator moved the resource from Bypass approval to Automatic", async () => {
453
+ const followUp: AutoRemediationSuggestion = {
454
+ ...suggestion(AutoRemediationExecutionMode.FullAuto),
455
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1" (round 2)',
456
+ } as unknown as AutoRemediationSuggestion;
457
+
458
+ const resolution: ResourceModeResolution =
459
+ await RemediationExecutionRunner.resolveResourceMode({
460
+ suggestion: followUp,
461
+ resource: resource({
462
+ aiRemediationMode: ResourceAiRemediationMode.Automatic,
463
+ }),
464
+ });
465
+
466
+ expect(resolution.mode).toBe("Suggest");
467
+ expect(resolution.downgradedByModeChange).toBe(true);
468
+ expect(countBy).not.toHaveBeenCalled();
469
+ });
470
+
471
+ it("negative control: a FullAuto follow-up round still on Bypass approval runs FullAuto", async () => {
472
+ const followUp: AutoRemediationSuggestion = {
473
+ ...suggestion(AutoRemediationExecutionMode.FullAuto),
474
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1" (round 2)',
475
+ } as unknown as AutoRemediationSuggestion;
476
+
477
+ const resolution: ResourceModeResolution =
478
+ await RemediationExecutionRunner.resolveResourceMode({
479
+ suggestion: followUp,
480
+ resource: resource({
481
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
482
+ }),
483
+ });
484
+
485
+ expect(resolution.mode).toBe("FullAuto");
486
+ expect(resolution.downgradedByModeChange).toBe(false);
487
+ });
488
+
489
+ it("negative control: a resource NAMED like a follow-up is still round 1, and Automatic runs it FullAuto", async () => {
490
+ const roundOne: AutoRemediationSuggestion = {
491
+ ...suggestion(AutoRemediationExecutionMode.FullAuto),
492
+ ruleNameSnapshot: 'AI remediation for Docker host "web (round 2)"',
493
+ } as unknown as AutoRemediationSuggestion;
494
+
495
+ const resolution: ResourceModeResolution =
496
+ await RemediationExecutionRunner.resolveResourceMode({
497
+ suggestion: roundOne,
498
+ resource: resource({
499
+ aiRemediationMode: ResourceAiRemediationMode.Automatic,
500
+ }),
501
+ });
502
+
503
+ expect(resolution.mode).toBe("FullAuto");
504
+ });
505
+
506
+ it("downgrades when the hourly breaker tripped, even under Bypass approval", async () => {
507
+ countBy.mockResolvedValue(
508
+ new PositiveNumber(MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR),
509
+ );
510
+
511
+ const resolution: ResourceModeResolution =
512
+ await RemediationExecutionRunner.resolveResourceMode({
513
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
514
+ resource: resource({
515
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
516
+ }),
517
+ });
518
+
519
+ expect(resolution).toMatchObject({
520
+ mode: "Suggest",
521
+ downgradedByCircuitBreaker: true,
522
+ breakerCheckFailed: false,
523
+ autoExecutedInWindow: MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR,
524
+ });
525
+ });
526
+
527
+ it("downgrades when the breaker cannot be checked", async () => {
528
+ countBy.mockRejectedValue(new Error("db down"));
529
+
530
+ const resolution: ResourceModeResolution =
531
+ await RemediationExecutionRunner.resolveResourceMode({
532
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
533
+ resource: resource(),
534
+ });
535
+
536
+ expect(resolution).toMatchObject({
537
+ mode: "Suggest",
538
+ downgradedByCircuitBreaker: true,
539
+ breakerCheckFailed: true,
540
+ });
541
+ });
542
+
543
+ it("downgrades when another round holds the resource, and names it", async () => {
544
+ suggestionFindBy.mockImplementation(
545
+ async (args: unknown): Promise<Array<AutoRemediationSuggestion>> => {
546
+ const query: Record<string, unknown> = (
547
+ args as { query: Record<string, unknown> }
548
+ ).query;
549
+ if (typeof query["status"] === "string") {
550
+ return [];
551
+ }
552
+ return [
553
+ {
554
+ id: OTHER_SUGGESTION_ID,
555
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
556
+ verificationStatus: AutoRemediationVerificationStatus.Pending,
557
+ verificationDeadlineAt: OneUptimeDate.addRemoveMinutes(
558
+ OneUptimeDate.getCurrentDate(),
559
+ 5,
560
+ ),
561
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1"',
562
+ } as unknown as AutoRemediationSuggestion,
563
+ ];
564
+ },
565
+ );
566
+
567
+ const resolution: ResourceModeResolution =
568
+ await RemediationExecutionRunner.resolveResourceMode({
569
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
570
+ resource: resource(),
571
+ });
572
+
573
+ expect(resolution.mode).toBe("Suggest");
574
+ expect(resolution.downgradedByInFlightRound).toBe(true);
575
+ expect(resolution.inFlightRound).toMatchObject({
576
+ suggestionId: OTHER_SUGGESTION_ID.toString(),
577
+ description: "applied a fix that is still being verified",
578
+ });
579
+ });
580
+
581
+ it("downgrades when the hold cannot be checked", async () => {
582
+ jobFindBy
583
+ .mockResolvedValueOnce([])
584
+ .mockRejectedValueOnce(new Error("db down"));
585
+
586
+ const resolution: ResourceModeResolution =
587
+ await RemediationExecutionRunner.resolveResourceMode({
588
+ suggestion: suggestion(AutoRemediationExecutionMode.FullAuto),
589
+ resource: resource(),
590
+ });
591
+
592
+ expect(resolution).toMatchObject({
593
+ mode: "Suggest",
594
+ downgradedByInFlightRound: true,
595
+ inFlightRound: null,
596
+ });
597
+ });
598
+
599
+ it("delegates the breaker to the rule engine's resource breaker", async () => {
600
+ const state: ResourceBreakerState =
601
+ await RemediationExecutionRunner.getResourceBreakerState({
602
+ resourceType: AiResourceType.Host,
603
+ resourceId: RESOURCE_ID.toString(),
604
+ });
605
+
606
+ expect(state).toEqual({ autoExecutedInWindow: 0, hasHeadroom: true });
607
+ });
608
+ });
609
+
610
+ describe("RemediationExecutionRunner.executeRemediation — resource rounds", () => {
611
+ let suggestionCas: jest.SpyInstance;
612
+ let suggestionUpdate: jest.SpyInstance;
613
+ let incidentFeed: jest.SpyInstance;
614
+ let statusForResource: jest.SpyInstance;
615
+ let countBy: jest.SpyInstance;
616
+ let suggestionFindBy: jest.SpyInstance;
617
+ let enqueue: jest.SpyInstance;
618
+ let project: jest.SpyInstance;
619
+
620
+ beforeEach(() => {
621
+ jest
622
+ .spyOn(AIRunService, "attemptStatusTransition")
623
+ .mockResolvedValue(1 as never);
624
+ jest
625
+ .spyOn(AIRunService, "updateOneBy")
626
+ .mockResolvedValue(undefined as never);
627
+ suggestionCas = jest
628
+ .spyOn(AutoRemediationSuggestionService, "attemptStatusTransition")
629
+ .mockResolvedValue(1 as never);
630
+ suggestionUpdate = jest
631
+ .spyOn(AutoRemediationSuggestionService, "updateOneById")
632
+ .mockResolvedValue(undefined as never);
633
+ incidentFeed = jest
634
+ .spyOn(IncidentFeedService, "createIncidentFeedItem")
635
+ .mockResolvedValue(undefined as never);
636
+ jest
637
+ .spyOn(AlertFeedService, "createAlertFeedItem")
638
+ .mockResolvedValue(undefined as never);
639
+ for (const level of ["debug", "warn", "error"] as const) {
640
+ jest.spyOn(logger, level).mockImplementation((): void => {
641
+ return undefined;
642
+ });
643
+ }
644
+ // The project did NOT opt into AI command execution: a resource round does not need it.
645
+ project = jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
646
+ id: PROJECT_ID,
647
+ enableAi: true,
648
+ enableAutoRemediation: true,
649
+ enableAiCommandExecution: false,
650
+ } as unknown as Project);
651
+ jest.spyOn(IncidentService, "findOneById").mockResolvedValue({
652
+ id: INCIDENT_ID,
653
+ title: "web-1 keeps exiting",
654
+ description: "OOMKilled",
655
+ incidentNumber: 7,
656
+ } as unknown as Incident);
657
+ jest.spyOn(PostedRootCause, "getForSubject").mockResolvedValue(null);
658
+ suggestionFindBy = jest
659
+ .spyOn(AutoRemediationSuggestionService, "findBy")
660
+ .mockResolvedValue([]);
661
+ countBy = jest
662
+ .spyOn(AutoRemediationSuggestionService, "countBy")
663
+ .mockResolvedValue(new PositiveNumber(0));
664
+ jest.spyOn(RunnerJobService, "findBy").mockResolvedValue([]);
665
+ jest
666
+ .spyOn(RunnerJobService, "countBy")
667
+ .mockResolvedValue(new PositiveNumber(0));
668
+ statusForResource = jest
669
+ .spyOn(ResourceAiAccessService, "getStatusForResource")
670
+ .mockResolvedValue(resource());
671
+ jest
672
+ .spyOn(ResourceAiAccessService, "recordCommandOutcome")
673
+ .mockResolvedValue(undefined);
674
+ enqueue = jest
675
+ .spyOn(RunnerJobService, "enqueueAiResourceCommand")
676
+ .mockResolvedValue({
677
+ id: JOB_ID,
678
+ status: RunnerJobStatus.Pending,
679
+ payload: { displayCommand: "docker restart web", program: "docker" },
680
+ } as unknown as RunnerJob);
681
+ jest.spyOn(RunnerJobService, "pollUntilTerminal").mockResolvedValue({
682
+ id: JOB_ID,
683
+ status: RunnerJobStatus.Succeeded,
684
+ exitCode: 0,
685
+ output: "web",
686
+ } as unknown as RunnerJob);
687
+ jest
688
+ .spyOn(Semaphore, "lock")
689
+ .mockResolvedValue({ id: "lock" } as unknown as SemaphoreMutex);
690
+ jest.spyOn(Semaphore, "release").mockResolvedValue(undefined);
691
+ });
692
+
693
+ afterEach(() => {
694
+ jest.restoreAllMocks();
695
+ });
696
+
697
+ it("runs an Automatic resource's round FullAuto with the resource toolkit, the read tool, the persona and the context", async () => {
698
+ const findOneById: jest.SpyInstance =
699
+ mockSuggestionHonouringSelect(resourceRow());
700
+ const ruleRead: jest.SpyInstance = jest.spyOn(
701
+ AutoRemediationRuleService,
702
+ "findOneById",
703
+ );
704
+ const clusterRead: jest.SpyInstance = jest.spyOn(
705
+ KubernetesClusterAiAccessService,
706
+ "getStatusForCluster",
707
+ );
708
+ const request: { get: () => InvestigationRequest } = captureRequest();
709
+
710
+ await run();
711
+
712
+ const select: Record<string, boolean> = (
713
+ findOneById.mock.calls[0]![0] as { select: Record<string, boolean> }
714
+ ).select;
715
+ expect(select["resourceType"]).toBe(true);
716
+ expect(select["resourceId"]).toBe(true);
717
+
718
+ expect(statusForResource).toHaveBeenCalledWith({
719
+ projectId: PROJECT_ID,
720
+ resourceType: AiResourceType.DockerHost,
721
+ resourceId: RESOURCE_ID,
722
+ });
723
+ expect(ruleRead).not.toHaveBeenCalled();
724
+ expect(clusterRead).not.toHaveBeenCalled();
725
+
726
+ expect(toolNames(request.get()).sort()).toEqual(
727
+ [
728
+ "list_command_targets",
729
+ "execute_remediation_command",
730
+ LIST_INFRASTRUCTURE_ACCESS_TOOL_NAME,
731
+ RUN_INFRASTRUCTURE_COMMAND_TOOL_NAME,
732
+ ].sort(),
733
+ );
734
+ expect(request.get().personaOverride).toContain(
735
+ "turned on Automatic remediation",
736
+ );
737
+ expect(request.get().personaOverride).toContain('Docker host "web-1"');
738
+ expect(request.get().questionOverride).toContain(
739
+ 'A signal has been declared on Docker host "web-1"',
740
+ );
741
+ expect(request.get().questionOverride).toContain("docker");
742
+
743
+ const context: string = request.get().contextSummary || "";
744
+ expect(context).toContain("# The resource");
745
+ expect(context).toContain(
746
+ `Docker host "web-1" (resourceId: ${RESOURCE_ID.toString()}), reached through its Docker AI agent (programs: docker)`,
747
+ );
748
+ expect(context).toContain("Remediation mode: Automatic");
749
+ expect(context).toContain(
750
+ "An unattended run becomes a proposal when the hourly per-resource circuit breaker trips",
751
+ );
752
+ expect(context).toContain("Where the Docker AI agent writes:");
753
+ expect(context).toContain("You may execute at most 5 commands");
754
+ expect(context).not.toContain("Matched auto-remediation rule");
755
+ expect(context).not.toContain("downgraded");
756
+
757
+ // Headroom: nothing to correct on the row, nothing on the feed.
758
+ expect(incidentFeed).not.toHaveBeenCalled();
759
+ expect(suggestionUpdate).not.toHaveBeenCalled();
760
+ });
761
+
762
+ it("runs a RequireApproval resource's round as a plan for approval", async () => {
763
+ mockSuggestionHonouringSelect(
764
+ resourceRow({ executionMode: AutoRemediationExecutionMode.Suggest }),
765
+ );
766
+ statusForResource.mockResolvedValue(
767
+ resource({
768
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
769
+ }),
770
+ );
771
+ const request: { get: () => InvestigationRequest } = captureRequest();
772
+
773
+ await run();
774
+
775
+ expect(toolNames(request.get())).toContain("propose_remediation_commands");
776
+ expect(toolNames(request.get())).not.toContain(
777
+ "execute_remediation_command",
778
+ );
779
+ expect(request.get().personaOverride).toContain("REMEDIATION PLANNING");
780
+ expect(request.get().questionOverride).toContain(
781
+ "compose a plan for human approval",
782
+ );
783
+ expect(request.get().contextSummary).toContain(
784
+ "Remediation mode: a human approves",
785
+ );
786
+ });
787
+
788
+ it("runs a BypassApproval resource with the bypass persona and question", async () => {
789
+ mockSuggestionHonouringSelect(resourceRow());
790
+ statusForResource.mockResolvedValue(
791
+ resource({
792
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
793
+ }),
794
+ );
795
+ const request: { get: () => InvestigationRequest } = captureRequest();
796
+
797
+ await run();
798
+
799
+ expect(request.get().personaOverride).toContain(
800
+ "chose to bypass approvals entirely",
801
+ );
802
+ expect(request.get().questionOverride).toContain(
803
+ "its operator bypassed approvals",
804
+ );
805
+ expect(request.get().contextSummary).toContain(
806
+ "Remediation mode: Bypass approval",
807
+ );
808
+ });
809
+
810
+ it("is stopped by the project kill switches like every round", async () => {
811
+ mockSuggestionHonouringSelect(resourceRow());
812
+ project.mockResolvedValue({
813
+ enableAi: true,
814
+ enableAutoRemediation: false,
815
+ } as unknown as Project);
816
+ const executeRun: jest.SpyInstance = jest.spyOn(
817
+ AIInvestigationEngine,
818
+ "executeRun",
819
+ );
820
+
821
+ await run();
822
+
823
+ expect(executeRun).not.toHaveBeenCalled();
824
+ expect(suggestionCas.mock.calls[0]![0]).toMatchObject({
825
+ set: { status: AutoRemediationSuggestionStatus.NoneApplicable },
826
+ });
827
+ });
828
+
829
+ it.each([
830
+ [
831
+ "no longer ready",
832
+ resource({
833
+ isRemediationReady: false,
834
+ gaps: [
835
+ {
836
+ code: "remediation_write_access_missing",
837
+ title: "The Docker AI agent is read-only",
838
+ nextStep: "Set ONEUPTIME_AI_ALLOW_WRITES=true.",
839
+ blocksInvestigation: false,
840
+ blocksRemediation: true,
841
+ },
842
+ ],
843
+ }),
844
+ 'OneUptime AI can no longer remediate Docker host "web-1": The Docker AI agent is read-only.',
845
+ ],
846
+ [
847
+ "deleted",
848
+ null,
849
+ 'OneUptime AI can no longer remediate Docker host "(deleted)".',
850
+ ],
851
+ ])(
852
+ "settles NoneApplicable, naming why, when the resource is %s",
853
+ async (
854
+ _label: string,
855
+ status: ResourceAiAccessStatus | null,
856
+ expected: string,
857
+ ) => {
858
+ mockSuggestionHonouringSelect(resourceRow());
859
+ statusForResource.mockResolvedValue(status);
860
+ const executeRun: jest.SpyInstance = jest.spyOn(
861
+ AIInvestigationEngine,
862
+ "executeRun",
863
+ );
864
+
865
+ await run();
866
+
867
+ expect(executeRun).not.toHaveBeenCalled();
868
+ const set: Record<string, unknown> = (
869
+ suggestionCas.mock.calls[0]![0] as { set: Record<string, unknown> }
870
+ ).set;
871
+ expect(set["status"]).toBe(
872
+ AutoRemediationSuggestionStatus.NoneApplicable,
873
+ );
874
+ expect(set["rationaleMarkdown"]).toContain(expected);
875
+ expect(set["rationaleMarkdown"]).toContain(
876
+ "Review the Docker host's AI agent page (AI → AI agent).",
877
+ );
878
+ },
879
+ );
880
+
881
+ it("a breaker downgrade corrects the row, tells the human on the feed and opens the rationale", async () => {
882
+ mockSuggestionHonouringSelect(resourceRow());
883
+ countBy.mockResolvedValue(
884
+ new PositiveNumber(MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR),
885
+ );
886
+ const request: { get: () => InvestigationRequest } = captureRequest();
887
+
888
+ await run();
889
+
890
+ expect(toolNames(request.get())).toContain("propose_remediation_commands");
891
+ expect(suggestionUpdate).toHaveBeenCalledWith(
892
+ expect.objectContaining({
893
+ id: SUGGESTION_ID,
894
+ data: {
895
+ executionMode: AutoRemediationExecutionMode.Suggest,
896
+ autoResolveOnRecovery: false,
897
+ },
898
+ }),
899
+ );
900
+ const markdown: string = (
901
+ incidentFeed.mock.calls[0]![0] as { feedInfoInMarkdown: string }
902
+ ).feedInfoInMarkdown;
903
+ expect(markdown).toContain(
904
+ 'AI remediation for Docker host "web-1": the hourly circuit breaker tripped',
905
+ );
906
+ expect(markdown).toContain(
907
+ `Docker host "web-1" already had ${MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR} unattended AI fix(es)`,
908
+ );
909
+ expect(request.get().contextSummary).toContain(
910
+ 'This round was downgraded to approval: The hourly circuit breaker for Docker host "web-1" tripped',
911
+ );
912
+
913
+ // The note opens the rationale when the round settles.
914
+ await request.get().postAnalysis!(postAnalysisArgs("**Summary** nothing"));
915
+ const set: Record<string, unknown> = (
916
+ suggestionCas.mock.calls[0]![0] as { set: Record<string, unknown> }
917
+ ).set;
918
+ expect(set["rationaleMarkdown"]).toMatch(
919
+ /^The hourly circuit breaker for Docker host "web-1" tripped/,
920
+ );
921
+ });
922
+
923
+ it("a mode change after the announcement is its own downgrade", async () => {
924
+ mockSuggestionHonouringSelect(resourceRow());
925
+ statusForResource.mockResolvedValue(
926
+ resource({
927
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
928
+ }),
929
+ );
930
+ captureRequest();
931
+
932
+ await run();
933
+
934
+ const markdown: string = (
935
+ incidentFeed.mock.calls[0]![0] as { feedInfoInMarkdown: string }
936
+ ).feedInfoInMarkdown;
937
+ expect(markdown).toContain(
938
+ 'The AI remediation mode of Docker host "web-1" was changed to "Ask for approval"',
939
+ );
940
+ });
941
+
942
+ it("a follow-up round moved from Bypass approval to Automatic is downgraded, and the feed says Automatic asks for follow-ups", async () => {
943
+ mockSuggestionHonouringSelect(
944
+ resourceRow({
945
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1" (round 2)',
946
+ }),
947
+ );
948
+ statusForResource.mockResolvedValue(
949
+ resource({ aiRemediationMode: ResourceAiRemediationMode.Automatic }),
950
+ );
951
+ captureRequest();
952
+
953
+ await run();
954
+
955
+ // The row stops claiming an unattended round.
956
+ expect(suggestionUpdate).toHaveBeenCalledWith(
957
+ expect.objectContaining({
958
+ data: expect.objectContaining({
959
+ executionMode: AutoRemediationExecutionMode.Suggest,
960
+ autoResolveOnRecovery: false,
961
+ }),
962
+ }),
963
+ );
964
+
965
+ const markdown: string = (
966
+ incidentFeed.mock.calls[0]![0] as { feedInfoInMarkdown: string }
967
+ ).feedInfoInMarkdown;
968
+ expect(markdown).toContain(
969
+ 'The AI remediation mode of Docker host "web-1" was changed to "Automatic" after this follow-up round was announced as unattended',
970
+ );
971
+ expect(markdown).toContain("asks for approval of every follow-up round");
972
+ });
973
+
974
+ it("settles AutoExecuted with verification once the round ran a change", async () => {
975
+ mockSuggestionHonouringSelect(resourceRow());
976
+ const request: { get: () => InvestigationRequest } = captureRequest();
977
+
978
+ await run();
979
+
980
+ const outcome: ToolCallOutcome = await findTool(
981
+ request.get(),
982
+ "execute_remediation_command",
983
+ ).execute(resourceArgs());
984
+ expect(outcome.success).toBe(true);
985
+ expect(enqueue).toHaveBeenCalledTimes(1);
986
+
987
+ await request.get().postAnalysis!(
988
+ postAnalysisArgs("**Summary** restarted web"),
989
+ );
990
+
991
+ const cas: Record<string, unknown> = suggestionCas.mock
992
+ .calls[0]![0] as Record<string, unknown>;
993
+ expect(cas["fromStatus"]).toBe(AutoRemediationSuggestionStatus.Planning);
994
+ const set: Record<string, unknown> = cas["set"] as Record<string, unknown>;
995
+ expect(set["status"]).toBe(AutoRemediationSuggestionStatus.AutoExecuted);
996
+ expect(set["verificationStatus"]).toBe(
997
+ AutoRemediationVerificationStatus.Pending,
998
+ );
999
+ const plan: AiRemediationCommandPlan = AiRemediationCommandPlanUtil.parse(
1000
+ set["commandPlan"] as JSONObject,
1001
+ )!;
1002
+ expect(plan.commands[0]).toMatchObject({
1003
+ stepType: RunbookStepType.ResourceCommand,
1004
+ resourceType: AiResourceType.DockerHost,
1005
+ resourceId: RESOURCE_ID.toString(),
1006
+ runnerId: AGENT_ID,
1007
+ });
1008
+ expect(plan.commands[0]!.execution?.status).toBe(
1009
+ AiRemediationCommandExecutionStatus.Succeeded,
1010
+ );
1011
+
1012
+ const markdowns: Array<string> = incidentFeed.mock.calls.map(
1013
+ (call: Array<unknown>): string => {
1014
+ return (call[0] as { feedInfoInMarkdown: string }).feedInfoInMarkdown;
1015
+ },
1016
+ );
1017
+ expect(markdowns[markdowns.length - 1]).toContain(
1018
+ 'AI remediation for Docker host "web-1": AI executed 1 command(s).',
1019
+ );
1020
+ expect(markdowns.join("\n")).not.toContain("Auto Remediation Rule");
1021
+ });
1022
+
1023
+ it("an unattended round that ran nothing proposes exactly the changes it kept for a human", async () => {
1024
+ mockSuggestionHonouringSelect(resourceRow());
1025
+ const request: { get: () => InvestigationRequest } = captureRequest();
1026
+
1027
+ await run();
1028
+
1029
+ const refused: ToolCallOutcome = await findTool(
1030
+ request.get(),
1031
+ "execute_remediation_command",
1032
+ ).execute(
1033
+ resourceArgs({
1034
+ command: "docker stop web",
1035
+ rollbackCommand: "docker start web",
1036
+ }),
1037
+ );
1038
+ expect(refused.success).toBe(false);
1039
+ expect(enqueue).not.toHaveBeenCalled();
1040
+
1041
+ await request.get().postAnalysis!(postAnalysisArgs("**Summary** stop it"));
1042
+
1043
+ // The row stops claiming an unattended round before it is proposed.
1044
+ expect(suggestionUpdate).toHaveBeenCalledWith(
1045
+ expect.objectContaining({
1046
+ data: {
1047
+ executionMode: AutoRemediationExecutionMode.Suggest,
1048
+ autoResolveOnRecovery: false,
1049
+ },
1050
+ }),
1051
+ );
1052
+
1053
+ const set: Record<string, unknown> = (
1054
+ suggestionCas.mock.calls[0]![0] as { set: Record<string, unknown> }
1055
+ ).set;
1056
+ expect(set["status"]).toBe(AutoRemediationSuggestionStatus.Suggested);
1057
+ expect(set["rationaleMarkdown"]).toContain(
1058
+ "OneUptime AI did not run the following change(s) on its own",
1059
+ );
1060
+ expect(set["rationaleMarkdown"]).not.toContain("kubectl");
1061
+ const plan: AiRemediationCommandPlan = AiRemediationCommandPlanUtil.parse(
1062
+ set["commandPlan"] as JSONObject,
1063
+ )!;
1064
+ expect(plan.commands).toHaveLength(1);
1065
+ expect(plan.commands[0]).toMatchObject({
1066
+ sequence: 1,
1067
+ stepType: RunbookStepType.ResourceCommand,
1068
+ command: "docker stop web",
1069
+ rollbackCommand: "docker start web",
1070
+ resourceCommandTier: ResourceCommandTier.RiskyWrite,
1071
+ policyVerdict: AiRemediationCommandPolicyVerdict.RequiresApproval,
1072
+ runnerId: AGENT_ID,
1073
+ });
1074
+
1075
+ const markdowns: Array<string> = incidentFeed.mock.calls.map(
1076
+ (call: Array<unknown>): string => {
1077
+ return (call[0] as { feedInfoInMarkdown: string }).feedInfoInMarkdown;
1078
+ },
1079
+ );
1080
+ expect(markdowns[markdowns.length - 1]).toContain(
1081
+ "AI needs your approval for 1 change(s) it did not run on its own",
1082
+ );
1083
+ });
1084
+
1085
+ it.each([
1086
+ [
1087
+ "stopped allowing remediation",
1088
+ resource({ isRemediationReady: false }),
1089
+ 'Docker host "web-1" stopped allowing AI remediation during this round',
1090
+ ],
1091
+ [
1092
+ "is reached through another agent",
1093
+ resource({
1094
+ agent: {
1095
+ agentId: OTHER_AGENT_ID,
1096
+ connectionStatus: "connected",
1097
+ isOnline: true,
1098
+ posture: posture(),
1099
+ },
1100
+ }),
1101
+ "is no longer reached through the Docker AI agent this round composed its changes for",
1102
+ ],
1103
+ [
1104
+ "has an agent that would refuse the change",
1105
+ resource({}, { protectedTargets: ["web"] }),
1106
+ 'The Docker AI agent of Docker host "web-1" would refuse every change this round kept',
1107
+ ],
1108
+ ["was deleted", null, 'Docker host "web-1" was deleted during this round'],
1109
+ ])(
1110
+ "proposes nothing when, at settle, the resource %s",
1111
+ async (
1112
+ _label: string,
1113
+ live: ResourceAiAccessStatus | null,
1114
+ expected: string,
1115
+ ) => {
1116
+ mockSuggestionHonouringSelect(resourceRow());
1117
+ const request: { get: () => InvestigationRequest } = captureRequest();
1118
+
1119
+ await run();
1120
+
1121
+ await findTool(request.get(), "execute_remediation_command").execute(
1122
+ resourceArgs({ command: "docker stop web" }),
1123
+ );
1124
+
1125
+ statusForResource.mockResolvedValue(live);
1126
+
1127
+ await request.get().postAnalysis!(postAnalysisArgs("**Summary** x"));
1128
+
1129
+ const set: Record<string, unknown> = (
1130
+ suggestionCas.mock.calls[0]![0] as { set: Record<string, unknown> }
1131
+ ).set;
1132
+ expect(set["status"]).toBe(
1133
+ AutoRemediationSuggestionStatus.NoneApplicable,
1134
+ );
1135
+ expect(set["rationaleMarkdown"]).toContain(expected);
1136
+ expect(set["rationaleMarkdown"]).toContain(
1137
+ "Review the Docker host's AI agent page (AI → AI agent).",
1138
+ );
1139
+ },
1140
+ );
1141
+
1142
+ it("shows the model the earlier rounds on this resource for this signal", async () => {
1143
+ mockSuggestionHonouringSelect(
1144
+ resourceRow({
1145
+ executionMode: AutoRemediationExecutionMode.Suggest,
1146
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1" (round 2)',
1147
+ }),
1148
+ );
1149
+ suggestionFindBy.mockResolvedValue([
1150
+ {
1151
+ id: OTHER_SUGGESTION_ID,
1152
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
1153
+ verificationStatus: AutoRemediationVerificationStatus.Failed,
1154
+ verificationNote: "Monitors still down.",
1155
+ commandPlan: {
1156
+ commands: [
1157
+ {
1158
+ sequence: 1,
1159
+ stepType: RunbookStepType.ResourceCommand,
1160
+ runnerId: AGENT_ID,
1161
+ runnerNameSnapshot: "Docker AI agent",
1162
+ resourceType: AiResourceType.DockerHost,
1163
+ resourceId: RESOURCE_ID.toString(),
1164
+ command: "docker restart web",
1165
+ rollbackCommand: "docker start web",
1166
+ timeoutInMs: 60000,
1167
+ rationale: "r",
1168
+ expectedEffect: "e",
1169
+ policyVerdict: AiRemediationCommandPolicyVerdict.AutoApproved,
1170
+ execution: {
1171
+ status: AiRemediationCommandExecutionStatus.Succeeded,
1172
+ },
1173
+ rollbackExecution: {
1174
+ status: AiRemediationCommandExecutionStatus.Failed,
1175
+ },
1176
+ },
1177
+ ],
1178
+ rollbackStatus: AiRemediationRollbackStatus.Failed,
1179
+ },
1180
+ } as unknown as AutoRemediationSuggestion,
1181
+ ]);
1182
+ const request: { get: () => InvestigationRequest } = captureRequest();
1183
+
1184
+ await run();
1185
+
1186
+ const context: string = request.get().contextSummary || "";
1187
+ expect(context).toContain(
1188
+ "# Previous remediation attempts on this Docker host for this signal",
1189
+ );
1190
+ expect(context).toContain("docker restart web");
1191
+ expect(context).toContain(
1192
+ `WARNING: this attempt's changes may STILL BE APPLIED on the Docker host — confirm the live state with ${RUN_INFRASTRUCTURE_COMMAND_TOOL_NAME} before acting.`,
1193
+ );
1194
+ expect(context).toContain(
1195
+ `diagnose the Docker host as it is now (${RUN_INFRASTRUCTURE_COMMAND_TOOL_NAME})`,
1196
+ );
1197
+
1198
+ const previousQuery: Record<string, unknown> = suggestionFindBy.mock.calls
1199
+ .map((call: Array<unknown>) => {
1200
+ return (call[0] as { query: Record<string, unknown> }).query;
1201
+ })
1202
+ .find((query: Record<string, unknown>) => {
1203
+ return "incidentId" in query;
1204
+ }) as Record<string, unknown>;
1205
+ expect(previousQuery).toMatchObject({
1206
+ incidentId: INCIDENT_ID,
1207
+ resourceType: AiResourceType.DockerHost,
1208
+ resourceId: RESOURCE_ID,
1209
+ });
1210
+ expect(previousQuery["kubernetesClusterId"]).toBeUndefined();
1211
+ });
1212
+
1213
+ it("the downgrade note names another round holding the resource", async () => {
1214
+ mockSuggestionHonouringSelect(resourceRow());
1215
+ suggestionFindBy.mockImplementation(
1216
+ async (args: unknown): Promise<Array<AutoRemediationSuggestion>> => {
1217
+ const query: Record<string, unknown> = (
1218
+ args as { query: Record<string, unknown> }
1219
+ ).query;
1220
+ if (typeof query["status"] === "string" || "incidentId" in query) {
1221
+ return [];
1222
+ }
1223
+ return [
1224
+ {
1225
+ id: OTHER_SUGGESTION_ID,
1226
+ status: AutoRemediationSuggestionStatus.Planning,
1227
+ executionMode: AutoRemediationExecutionMode.FullAuto,
1228
+ createdAt: OneUptimeDate.getSomeMinutesAgo(30),
1229
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1"',
1230
+ } as unknown as AutoRemediationSuggestion,
1231
+ ];
1232
+ },
1233
+ );
1234
+ const request: { get: () => InvestigationRequest } = captureRequest();
1235
+
1236
+ await run();
1237
+
1238
+ expect(request.get().contextSummary).toContain(
1239
+ 'another OneUptime AI round on Docker host "web-1" (AI remediation for Docker host "web-1") is still running unattended',
1240
+ );
1241
+ expect(request.get().contextSummary).toContain(
1242
+ "This round was downgraded to approval",
1243
+ );
1244
+ // A Suggest round's context no longer promises an unattended run.
1245
+ expect(request.get().contextSummary).not.toContain(
1246
+ RESOURCE_UNATTENDED_ROUND_BECOMES_PROPOSAL_SUMMARY,
1247
+ );
1248
+ });
1249
+ });