@oneuptime/common 14.0.8 → 14.0.9

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 (639) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  4. package/Models/DatabaseModels/Project.ts +55 -5
  5. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  6. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  7. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  8. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  9. package/Server/API/AIChatAPI.ts +6 -3
  10. package/Server/API/AIInvestigationAPI.ts +17 -10
  11. package/Server/API/AutoRemediationAPI.ts +207 -25
  12. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  13. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  14. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  15. package/Server/Infrastructure/GlobalCache.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/TelemetryIngest.ts +41 -18
  21. package/Server/Services/AIService.ts +70 -1
  22. package/Server/Services/AlertService.ts +352 -11
  23. package/Server/Services/AlertSeverityService.ts +65 -15
  24. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  25. package/Server/Services/IncidentService.ts +280 -104
  26. package/Server/Services/IncidentSeverityService.ts +66 -15
  27. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  28. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  29. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  30. package/Server/Services/KubernetesClusterService.ts +129 -21
  31. package/Server/Services/ProjectService.ts +32 -0
  32. package/Server/Services/ProxmoxResourceService.ts +322 -5
  33. package/Server/Services/RunnerJobService.ts +241 -104
  34. package/Server/Services/RunnerService.ts +1 -1
  35. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  36. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  37. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  38. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  39. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  40. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  41. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  42. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  43. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  44. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  45. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  46. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  47. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  48. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  49. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  50. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  51. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  52. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  53. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  54. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  55. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  56. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  57. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  58. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  59. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  60. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  61. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  62. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  63. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  64. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  65. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  66. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  67. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  68. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  69. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  70. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  71. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  72. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  73. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  74. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  75. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  76. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  77. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  78. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  79. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  80. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  81. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  82. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  83. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  84. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  85. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  86. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  87. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  88. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  89. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  90. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  91. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  92. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  93. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  94. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  95. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  96. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  97. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  98. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  99. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  100. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  101. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  102. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  103. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  104. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  105. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  106. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  107. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  108. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  109. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  110. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  111. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  112. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  113. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  114. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  115. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  116. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  117. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  118. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  119. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  120. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  121. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  122. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  123. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  124. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  125. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  126. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  127. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  128. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  129. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  130. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  131. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  132. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  133. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  134. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  135. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  136. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  137. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  138. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  139. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  140. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  141. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  142. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  143. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  144. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  145. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  146. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  147. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  148. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  149. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  150. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  151. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  152. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  153. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  154. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  155. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  156. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  157. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  158. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  159. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  160. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  161. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  162. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  163. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  164. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  165. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  166. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  167. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  168. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  169. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  170. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  171. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  172. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  173. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  174. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  175. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  176. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  177. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  178. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  179. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  180. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  181. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  182. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  183. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  184. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  185. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  186. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  187. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  188. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  189. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  190. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  191. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  192. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  193. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  194. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  195. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  196. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  197. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  198. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  199. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  200. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  201. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  202. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  203. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  204. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  205. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  206. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  207. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  208. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  209. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  210. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  211. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  212. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  213. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  214. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  215. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  216. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  217. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  218. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  219. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  220. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  221. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  222. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  223. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  224. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  225. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  226. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  227. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  228. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  229. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  230. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  231. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  232. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  233. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  234. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  235. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  236. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  237. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  238. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  239. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  240. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  241. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  242. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  243. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  244. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  245. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  246. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  247. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  248. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  249. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  250. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  251. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  252. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  253. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  254. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  255. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  256. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  257. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  258. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  259. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  260. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  261. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  262. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  263. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  264. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  265. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  266. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  267. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  268. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  269. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  270. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  271. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  272. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  273. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  274. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  275. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  276. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  277. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  278. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  279. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  280. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  281. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  282. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  283. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  284. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  285. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  286. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  287. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  288. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  289. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  290. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  291. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  292. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  293. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  294. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  295. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  296. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  297. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  298. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  299. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  300. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  301. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  302. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  303. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  304. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  305. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  306. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  307. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  308. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  309. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  310. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  311. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  312. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  313. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  314. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  315. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  316. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  317. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  318. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  319. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  320. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  321. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  322. package/Tests/UI/Components/Input.test.tsx +80 -1
  323. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  324. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  325. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  326. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  327. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  328. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  329. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  330. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  331. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  332. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  333. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  334. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  335. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  336. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  337. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  338. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  339. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  340. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  341. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  342. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  343. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  344. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  345. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  346. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  347. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  348. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  349. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  350. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  351. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  352. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  353. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  354. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  355. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  356. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  357. package/Types/AI/AIRunType.ts +9 -4
  358. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  359. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  360. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  361. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  362. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  363. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  364. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  365. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  366. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  367. package/Types/Team/ComplianceRule.ts +434 -0
  368. package/Types/Team/ComplianceRuleType.ts +21 -0
  369. package/Types/Team/TeamComplianceStatus.ts +81 -0
  370. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  371. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  372. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  373. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  374. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  375. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  376. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  377. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  378. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  379. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  380. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  381. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  382. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  383. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  384. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  385. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  386. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  387. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  388. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  389. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  390. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  391. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  392. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  393. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  394. package/UI/Components/Input/Input.tsx +90 -57
  395. package/UI/Components/List/ListRow.tsx +8 -74
  396. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  397. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  398. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  399. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  400. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  401. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  402. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  403. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  404. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  405. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  406. package/UI/Components/Table/TableBody.tsx +8 -2
  407. package/UI/Components/Table/TableRow.tsx +20 -140
  408. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  409. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  410. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  411. package/UI/Components/TelemetryViewer/types.ts +2 -0
  412. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  413. package/UI/Components/Toggle/Toggle.tsx +26 -0
  414. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  415. package/UI/Styles/Theme.css +14 -2
  416. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  417. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  418. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  419. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  420. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  421. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  422. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  423. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  424. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  425. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  426. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  427. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  428. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  429. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  430. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  431. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  432. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  433. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  434. package/build/dist/Server/API/AIChatAPI.js +6 -1
  435. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  436. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  437. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  438. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  439. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  440. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  441. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  442. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  443. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  444. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  445. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  446. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  447. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  456. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  457. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  458. package/build/dist/Server/Services/AIService.js +59 -1
  459. package/build/dist/Server/Services/AIService.js.map +1 -1
  460. package/build/dist/Server/Services/AlertService.js +248 -13
  461. package/build/dist/Server/Services/AlertService.js.map +1 -1
  462. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  463. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  464. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  465. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  466. package/build/dist/Server/Services/IncidentService.js +199 -72
  467. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  468. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  469. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  470. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  471. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  472. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  473. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  474. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  475. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  476. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  477. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  478. package/build/dist/Server/Services/ProjectService.js +22 -0
  479. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  480. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  481. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  482. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  483. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  484. package/build/dist/Server/Services/RunnerService.js +1 -1
  485. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  486. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  487. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  489. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  490. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  491. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  492. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  493. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  494. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  495. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  496. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  497. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  498. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  499. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  500. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  501. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  502. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  503. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  504. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  505. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  506. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  507. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  508. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  509. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  510. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  512. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  513. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  514. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  515. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  516. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  517. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  518. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  519. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  520. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  521. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  522. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  523. package/build/dist/Types/AI/AIRunType.js +9 -4
  524. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  525. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  526. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  527. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  528. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  529. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  530. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  531. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  532. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  533. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  534. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  535. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  536. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  537. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  538. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  539. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  540. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  541. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  542. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  543. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  544. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  545. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  546. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  547. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  548. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  549. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  550. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  551. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  552. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  553. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  554. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  556. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  558. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  560. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  562. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  564. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  570. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  571. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  572. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  574. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  575. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  576. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  578. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  579. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  580. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  582. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  583. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  584. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  585. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  586. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  587. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  588. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  589. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  590. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  591. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  592. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  593. package/build/dist/UI/Components/Input/Input.js +69 -54
  594. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  595. package/build/dist/UI/Components/List/ListRow.js +4 -39
  596. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  597. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  598. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  600. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  601. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  602. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  603. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  604. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  605. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  606. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  607. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  608. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  609. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  610. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  611. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  612. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  613. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  614. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  615. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  616. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  617. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  618. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  619. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  620. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  621. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  622. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  626. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  627. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  628. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  629. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  630. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  631. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  632. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  633. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  634. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  635. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  636. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  637. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  638. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  639. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import Alert from "../../Models/DatabaseModels/Alert";
2
2
  import Incident from "../../Models/DatabaseModels/Incident";
3
+ import KubernetesAiAgent from "../../Models/DatabaseModels/KubernetesAiAgent";
3
4
  import KubernetesCluster from "../../Models/DatabaseModels/KubernetesCluster";
5
+ import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
4
6
  import Monitor from "../../Models/DatabaseModels/Monitor";
5
7
  import Project from "../../Models/DatabaseModels/Project";
6
8
  import RunbookCredential from "../../Models/DatabaseModels/RunbookCredential";
@@ -24,23 +26,33 @@ import {
24
26
  getRunnerLiveStatus,
25
27
  } from "../../Types/Runner/RunnerLiveStatus";
26
28
  import {
29
+ KUBECTL_ALLOW_WRITES_ENV,
30
+ KUBECTL_WRITE_NAMESPACES_ENV,
27
31
  KUBERNETES_AGENT_RUNNER_NAME_PREFIX,
32
+ KUBERNETES_AI_AGENT_COMPONENT,
33
+ KUBERNETES_AI_AGENT_DISPLAY_NAME,
34
+ KubernetesAgentPosture,
28
35
  KubernetesAgentRegistrationRefusalReason,
29
36
  KubernetesAgentRunnerBindingState,
30
37
  KubernetesAiAccessGap,
38
+ KubernetesAiAccessRunnerSummary,
39
+ KubernetesAiAutomaticInvestigationSettings,
31
40
  KubernetesAiRemediationMode,
32
41
  KubernetesClusterAiAccessStatus,
33
42
  KubernetesRunnerPosture,
34
43
  isInClusterPostureForCluster,
35
44
  isSameKubernetesClusterIdentifier,
36
45
  normalizeKubernetesClusterIdentifier,
46
+ parseKubernetesAgentPosture,
37
47
  parseKubernetesRunnerPosture,
38
48
  } from "../../Types/Kubernetes/KubernetesClusterAiAccess";
39
49
  import { KubernetesClusterFeedEventType } from "../../Models/DatabaseModels/KubernetesClusterFeed";
40
50
  import { Blue500, Green500, Yellow500 } from "../../Types/BrandColors";
41
51
  import QueryHelper from "../Types/Database/QueryHelper";
52
+ import AIService from "./AIService";
42
53
  import AlertService from "./AlertService";
43
54
  import IncidentService from "./IncidentService";
55
+ import KubernetesAiAgentService from "./KubernetesAiAgentService";
44
56
  import KubernetesClusterFeedService from "./KubernetesClusterFeedService";
45
57
  import KubernetesClusterService from "./KubernetesClusterService";
46
58
  import LlmProviderService from "./LlmProviderService";
@@ -73,14 +85,22 @@ export {
73
85
  * Answers, from CURRENT configuration, whether AI can reach a cluster with
74
86
  * kubectl and what it may do there — and turns every reason it cannot into a
75
87
  * row of a checklist with a next step. The same status feeds the cluster's
76
- * AI page, the investigation panel ("we could not access the cluster, here
77
- * is why") and the AI runs themselves (which only ever act on a cluster this
78
- * module calls ready).
88
+ * AI agent page, the investigation panel ("we could not access the cluster,
89
+ * here is why") and the AI runs themselves (which only ever act on a cluster
90
+ * this module calls ready).
79
91
  *
80
- * It also binds the in-cluster Runner the kubernetes-agent chart installs:
81
- * that Runner registers with the project's telemetry ingestion key and the
82
- * cluster's name, and this module upserts its Runner row and points the
83
- * cluster at it. One helm flag, no dashboard steps.
92
+ * AI reaches a cluster through ONE access target, resolved from liveness by
93
+ * resolveKubernetesAiAccessTarget: the cluster's Kubernetes AI agent (its
94
+ * own identity, a KubernetesAiAgent row), the chart's previous in-cluster
95
+ * Runner, or a Runner an operator bound together with a Kubernetes
96
+ * credential. The status, the enqueue chokepoint and the access test all
97
+ * ask the same function, so they never disagree about which one it is.
98
+ *
99
+ * It also still registers the previous in-cluster Runner older charts
100
+ * install: that Runner registers with the project's telemetry ingestion key
101
+ * and the cluster's name, and this module upserts its Runner row and points
102
+ * the cluster at it — unless the cluster's Kubernetes AI agent is online,
103
+ * which replaces it.
84
104
  */
85
105
 
86
106
  export interface KubernetesClusterAiAccessProjectGates {
@@ -88,6 +108,20 @@ export interface KubernetesClusterAiAccessProjectGates {
88
108
  isAutoRemediationEnabled: boolean;
89
109
  isAiCommandExecutionEnabled: boolean;
90
110
  hasLlmProvider: boolean;
111
+ /*
112
+ * Why no AI run can start for lack of AI credits (AIService.
113
+ * getAiBalanceBlocker), or null/absent when nothing blocks. getProjectGates
114
+ * always sets it; callers that build gates by hand may leave it out.
115
+ */
116
+ aiBalanceBlocker?: string | null | undefined;
117
+ /*
118
+ * The project's automatic-investigation opt-ins, shown on the AI agent
119
+ * page as a line of their own (never a gap). getProjectGates always sets
120
+ * them; absent reads as both off.
121
+ */
122
+ automaticInvestigation?:
123
+ | KubernetesAiAutomaticInvestigationSettings
124
+ | undefined;
91
125
  }
92
126
 
93
127
  export interface RegisterKubernetesAgentRunnerResult {
@@ -140,16 +174,214 @@ export const CLUSTER_AI_ACCESS_SELECT: Record<string, boolean> = {
140
174
  const MAX_CLUSTERS_PER_SUBJECT: number = 10;
141
175
 
142
176
  /*
143
- * What to do about an in-cluster Runner that reports read-only RBAC while
144
- * remediation is on. It points at the AI page's write-access section, which
145
- * carries the complete helm commands (a bare --set line would miss the
146
- * chart index refresh and --reuse-values), and names the two values that
147
- * bound what the write role reaches: the namespaces it is bound in and the
148
- * node switch. Shown verbatim on incident pages too, so it names the page
149
- * rather than saying "this page".
177
+ * Where every next step sends an operator: the cluster's AI agent page
178
+ * (AI → Agent in the cluster's side menu). Shown verbatim on incident pages
179
+ * and in the investigation panel too, so it always names the page rather
180
+ * than saying "this page".
181
+ */
182
+ export const CLUSTER_AI_AGENT_PAGE: string =
183
+ "the cluster's AI agent page (AI → Agent)";
184
+
185
+ /*
186
+ * The one command that installs the Kubernetes AI agent (or turns it back
187
+ * on), exactly as the dashboard, the docs and the chart's notes print it.
188
+ * --reuse-values keeps everything else the release already has.
150
189
  */
151
- export const REMEDIATION_WRITE_ACCESS_NEXT_STEP: string =
152
- 'Grant the in-cluster Runner write access with a helm upgrade that adds --set aiAccess.remediation.enabled=true; the complete commands are under "Let AI apply fixes (write access)" on the cluster\'s AI page. List the namespaces AI may fix in aiAccess.remediation.namespaces (without it the write role is bound cluster-wide), and add aiAccess.remediation.nodeOperations=false to keep fixes off nodes.';
190
+ export const AI_AGENT_INSTALL_COMMAND: string = [
191
+ "helm repo update",
192
+ "helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\",
193
+ " --namespace oneuptime-agent --reuse-values \\",
194
+ " --set aiAgent.enabled=true",
195
+ ].join("\n");
196
+
197
+ /*
198
+ * How to read the agent pod's logs. The namespace is the one the agent
199
+ * reported (posture.podNamespace) when it is known, a placeholder otherwise.
200
+ */
201
+ export function getAiAgentLogsCommand(namespace?: string | undefined): string {
202
+ return `kubectl logs -n ${
203
+ namespace?.trim() || "<namespace>"
204
+ } -l component=${KUBERNETES_AI_AGENT_COMPONENT} --tail=100`;
205
+ }
206
+
207
+ /*
208
+ * The next step for a cluster no access target can reach: install the
209
+ * agent, with the command verbatim, or check its pod if that was done.
210
+ */
211
+ export const AI_AGENT_NOT_CONNECTED_NEXT_STEP: string = `Install the Kubernetes AI agent (use your own release name and namespace if they differ):\n\n${AI_AGENT_INSTALL_COMMAND}\n\nAlready installed? Check its logs: ${getAiAgentLogsCommand()}`;
212
+
213
+ /*
214
+ * Who runs kubectl for a cluster, as the chart-value wording tells them
215
+ * apart: the Kubernetes AI agent (aiAgent.* values), the previous in-cluster
216
+ * Runner an older chart installed (its values are aiAccess.*, and the fix is
217
+ * upgrading the chart to the AI agent), or a Runner an operator runs with a
218
+ * Kubernetes credential (its own ONEUPTIME_KUBECTL_* environment).
219
+ */
220
+ export type KubernetesAiAccessExecutorKind =
221
+ | "ai_agent"
222
+ | "legacy_runner"
223
+ | "credential_runner";
224
+
225
+ /*
226
+ * What to do about a read-only executor while remediation is on. For the
227
+ * agent it names the value that grants writes and the two that bound them
228
+ * (the namespaces the write role is bound in and the node switch); the
229
+ * complete command is on the AI agent page (a bare --set line would miss
230
+ * the chart index refresh and --reuse-values).
231
+ */
232
+ export const REMEDIATION_WRITE_ACCESS_NEXT_STEP: string = `Upgrade the Kubernetes agent chart with --set aiAgent.remediation.enabled=true; the complete command is on ${CLUSTER_AI_AGENT_PAGE}. List the namespaces AI may fix in aiAgent.remediation.namespaces (without it the write role is cluster-wide), and add aiAgent.remediation.nodeOperations=false to keep fixes off nodes.`;
233
+
234
+ // The same for the previous in-cluster Runner: the AI agent replaces it.
235
+ export const LEGACY_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP: string = `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over. Add --set aiAgent.remediation.enabled=true to the upgrade to let it apply fixes; the complete command is on ${CLUSTER_AI_AGENT_PAGE}.`;
236
+
237
+ // The same for a Runner no chart configures: its own environment.
238
+ export const CREDENTIAL_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP: string = `Set ${KUBECTL_ALLOW_WRITES_ENV}=true on the Runner's host and restart it (${KUBECTL_WRITE_NAMESPACES_ENV} limits the namespaces it may change).`;
239
+
240
+ export function getRemediationWriteAccessNextStep(
241
+ kind: KubernetesAiAccessExecutorKind,
242
+ ): string {
243
+ switch (kind) {
244
+ case "ai_agent":
245
+ return REMEDIATION_WRITE_ACCESS_NEXT_STEP;
246
+ case "legacy_runner":
247
+ return LEGACY_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP;
248
+ default:
249
+ return CREDENTIAL_RUNNER_REMEDIATION_WRITE_ACCESS_NEXT_STEP;
250
+ }
251
+ }
252
+
253
+ /*
254
+ * The one access target AI reaches a cluster through right now (see
255
+ * resolveKubernetesAiAccessTarget), with the row it was resolved from.
256
+ *
257
+ * advanced_runner: a Runner an operator bound that is not a kubernetes-agent
258
+ * row — reached with the cluster's Kubernetes credential.
259
+ * ai_agent: the cluster's Kubernetes AI agent (online or not).
260
+ * legacy_runner: the cluster's previous in-cluster Runner, bound to it.
261
+ * none: nothing can reach the cluster.
262
+ */
263
+ export type KubernetesAiAccessTarget =
264
+ | { type: "advanced_runner"; runner: Runner; isOnline: boolean }
265
+ | { type: "ai_agent"; agent: KubernetesAiAgent; isOnline: boolean }
266
+ | { type: "legacy_runner"; runner: Runner; isOnline: boolean }
267
+ | { type: "none" };
268
+
269
+ // The id jobs and plans name the target by (runnerId), or null for none.
270
+ export function getKubernetesAiAccessTargetId(
271
+ target: KubernetesAiAccessTarget,
272
+ ): string | null {
273
+ switch (target.type) {
274
+ case "ai_agent":
275
+ return target.agent.id?.toString() || null;
276
+ case "advanced_runner":
277
+ case "legacy_runner":
278
+ return target.runner.id?.toString() || null;
279
+ default:
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /*
285
+ * THE rule for which access target AI uses for a cluster. Liveness based,
286
+ * so a helm upgrade to the AI agent switches over the moment the agent is
287
+ * online, and a rollback that stops the agent falls back on its own to the
288
+ * previous in-cluster Runner the cluster is still bound to — no dashboard
289
+ * step either way:
290
+ *
291
+ * 1. a bound Runner that is not a kubernetes-agent row: the advanced Runner
292
+ * an operator chose (with its Kubernetes credential). An explicit
293
+ * operator choice wins over everything the chart installs.
294
+ * 2. the cluster's AI agent, when online.
295
+ * 3. the bound Runner, when it is THIS cluster's previous in-cluster Runner
296
+ * and online (the agent is not, or was never installed).
297
+ * 4. the cluster's AI agent, offline (its offline gap says what to check).
298
+ * 5. the bound previous in-cluster Runner, offline.
299
+ * 6. nothing.
300
+ *
301
+ * A bound Runner that is another cluster's kubernetes-agent row is none of
302
+ * these: its ServiceAccount can never reach this cluster. Pure: liveness is
303
+ * decided by the caller (isRunnerOnline, KubernetesAiAgentService.isOnline).
304
+ */
305
+ export function resolveKubernetesAiAccessTarget(data: {
306
+ clusterIdentifier: string | undefined;
307
+ aiAccessRunnerId: ObjectID | string | null | undefined;
308
+ // The Runner row aiAccessRunnerId names, when it still exists.
309
+ boundRunner: Runner | null | undefined;
310
+ isBoundRunnerOnline: boolean;
311
+ // The cluster's KubernetesAiAgent row, if one ever registered.
312
+ agent: KubernetesAiAgent | null | undefined;
313
+ isAgentOnline: boolean;
314
+ }): KubernetesAiAccessTarget {
315
+ /*
316
+ * Only the row the binding names: a row with another id (a stale read)
317
+ * is not the bound Runner.
318
+ */
319
+ const boundRunner: Runner | null =
320
+ data.aiAccessRunnerId &&
321
+ data.boundRunner &&
322
+ (!data.boundRunner.id ||
323
+ data.boundRunner.id.toString() === data.aiAccessRunnerId.toString())
324
+ ? data.boundRunner
325
+ : null;
326
+
327
+ if (
328
+ boundRunner &&
329
+ !RunnerServiceClass.isKubernetesAgentRunnerRow(boundRunner)
330
+ ) {
331
+ return {
332
+ type: "advanced_runner",
333
+ runner: boundRunner,
334
+ isOnline: data.isBoundRunnerOnline,
335
+ };
336
+ }
337
+
338
+ const legacyRunner: Runner | null =
339
+ boundRunner &&
340
+ RunnerServiceClass.isKubernetesAgentRunnerOfCluster(
341
+ boundRunner,
342
+ data.clusterIdentifier,
343
+ )
344
+ ? boundRunner
345
+ : null;
346
+
347
+ if (data.agent && data.isAgentOnline) {
348
+ return { type: "ai_agent", agent: data.agent, isOnline: true };
349
+ }
350
+
351
+ if (legacyRunner && data.isBoundRunnerOnline) {
352
+ return { type: "legacy_runner", runner: legacyRunner, isOnline: true };
353
+ }
354
+
355
+ if (data.agent) {
356
+ return { type: "ai_agent", agent: data.agent, isOnline: false };
357
+ }
358
+
359
+ if (legacyRunner) {
360
+ return { type: "legacy_runner", runner: legacyRunner, isOnline: false };
361
+ }
362
+
363
+ return { type: "none" };
364
+ }
365
+
366
+ // What the access-target resolution read, for callers that need the rows.
367
+ export interface LoadedKubernetesAiAccessTarget {
368
+ target: KubernetesAiAccessTarget;
369
+ // The Runner row aiAccessRunnerId names; null when unset or deleted.
370
+ boundRunner: Runner | null;
371
+ agent: KubernetesAiAgent | null;
372
+ }
373
+
374
+ // What target resolution and the status read off a bound Runner row.
375
+ const BOUND_RUNNER_SELECT: Record<string, boolean> = {
376
+ _id: true,
377
+ name: true,
378
+ lastAlive: true,
379
+ connectionStatus: true,
380
+ canRunAiCommands: true,
381
+ canRunRunbooks: true,
382
+ canRunCodeFixTasks: true,
383
+ hostInfo: true,
384
+ };
153
385
 
154
386
  /*
155
387
  * Why deleting an in-cluster Runner is a two-step remedy (the helper lives
@@ -189,6 +421,21 @@ export class KubernetesAgentRegistrationRefusedException extends ForbiddenExcept
189
421
  }
190
422
  }
191
423
 
424
+ /*
425
+ * How long a previous in-cluster Runner refused because the cluster's
426
+ * Kubernetes AI agent is online waits before it asks again. The refusal
427
+ * lasts only while the agent is online, so the Runner keeps asking — slowly
428
+ * — and is admitted within a minute of the agent stopping (a helm rollback).
429
+ */
430
+ export const SUPERSEDED_BY_AI_AGENT_RETRY_AFTER_SECONDS: number = 60;
431
+
432
+ // Why the previous in-cluster Runner was refused: the AI agent replaces it.
433
+ export function getSupersededByAiAgentMessage(
434
+ clusterIdentifier: string,
435
+ ): string {
436
+ return `The Kubernetes AI agent of cluster "${clusterIdentifier}" is online and replaces this in-cluster Runner, so the Runner was not registered. Nothing needs to be done: upgrading the Kubernetes agent chart removes the Runner, and it is admitted again only if the AI agent stops (for example after a helm rollback).`;
437
+ }
438
+
192
439
  /*
193
440
  * When a registration refused because the previous instance is online may
194
441
  * try again: once that instance's last heartbeat is older than the alive
@@ -245,6 +492,23 @@ interface RunnerPresence {
245
492
  isOnline: boolean;
246
493
  }
247
494
 
495
+ /*
496
+ * How the resolved access target reaches the cluster, for the status: the
497
+ * target's summary (runner), how kubectl authenticates, the credential when
498
+ * it is one, and every gap that stops the target itself.
499
+ */
500
+ interface ResolvedTargetAccess {
501
+ runner: KubernetesAiAccessRunnerSummary | null;
502
+ accessMethod: KubernetesClusterAiAccessStatus["accessMethod"];
503
+ credentialId?: string | undefined;
504
+ credentialName?: string | undefined;
505
+ gaps: Array<KubernetesAiAccessGap>;
506
+ }
507
+
508
+ // What to do about an empty AI balance (ai_balance_insufficient).
509
+ export const AI_BALANCE_INSUFFICIENT_NEXT_STEP: string =
510
+ "Add AI credits under Project Settings → AI Credits (or enable auto-recharge).";
511
+
248
512
  class KubernetesClusterAiAccessServiceClass {
249
513
  /*
250
514
  * ------------------------------------------------------------------
@@ -262,28 +526,58 @@ class KubernetesClusterAiAccessServiceClass {
262
526
  enableAi: true,
263
527
  enableAutoRemediation: true,
264
528
  enableAiCommandExecution: true,
529
+ enableAutomaticIncidentInvestigation: true,
530
+ enableAutomaticAlertInvestigation: true,
265
531
  },
266
532
  props: { isRoot: true },
267
533
  });
268
534
 
269
- let hasLlmProvider: boolean = false;
535
+ // undefined: the lookup failed, so neither it nor the balance is known.
536
+ let llmProvider: LlmProvider | null | undefined = undefined;
270
537
 
271
538
  try {
272
- hasLlmProvider =
273
- (await LlmProviderService.getLLMProviderForProject(projectId)) !== null;
539
+ llmProvider =
540
+ await LlmProviderService.getLLMProviderForProject(projectId);
274
541
  } catch (error) {
275
542
  logger.error(
276
543
  `KubernetesClusterAiAccess: could not resolve the LLM provider for project ${projectId.toString()}: ${error}`,
277
544
  );
278
545
  }
279
546
 
547
+ /*
548
+ * An empty balance is a precondition like the provider: when it cannot
549
+ * be checked, the status says nothing about it rather than claiming a
550
+ * blocker that may not exist (the AI call itself still refuses). The
551
+ * provider resolved above is handed over, so it is looked up once.
552
+ */
553
+ let aiBalanceBlocker: string | null = null;
554
+
555
+ if (llmProvider !== undefined) {
556
+ try {
557
+ aiBalanceBlocker = await AIService.getAiBalanceBlocker({
558
+ projectId,
559
+ llmProvider,
560
+ });
561
+ } catch (error) {
562
+ logger.error(
563
+ `KubernetesClusterAiAccess: could not check the AI balance of project ${projectId.toString()}: ${error}`,
564
+ );
565
+ }
566
+ }
567
+
280
568
  return {
281
569
  // Kill switches: a missing column counts as enabled (=== false idiom).
282
570
  isAiEnabled: project?.enableAi !== false,
283
571
  isAutoRemediationEnabled: project?.enableAutoRemediation !== false,
284
572
  // Explicit opt-in (=== true idiom), same as the Bash/SSH lane.
285
573
  isAiCommandExecutionEnabled: project?.enableAiCommandExecution === true,
286
- hasLlmProvider,
574
+ // A failed lookup reads as none, like no provider at all.
575
+ hasLlmProvider: Boolean(llmProvider),
576
+ aiBalanceBlocker,
577
+ automaticInvestigation: {
578
+ incidents: project?.enableAutomaticIncidentInvestigation === true,
579
+ alerts: project?.enableAutomaticAlertInvestigation === true,
580
+ },
287
581
  };
288
582
  }
289
583
 
@@ -314,254 +608,221 @@ class KubernetesClusterAiAccessServiceClass {
314
608
  }
315
609
 
316
610
  /*
317
- * The readiness computation proper. Reads the bound Runner and credential
318
- * as root (the cluster row is already tenant-scoped by the caller) and
319
- * folds project gates in. Never throws: a lookup failure becomes a gap,
320
- * because "we could not check" must not read as "ready".
611
+ * ------------------------------------------------------------------
612
+ * The access target
613
+ * ------------------------------------------------------------------
614
+ */
615
+
616
+ /*
617
+ * Is this Runner online right now? One rule for readiness, target
618
+ * resolution and registration: it heartbeated within the alive window
619
+ * (RunnerLiveStatus) AND it has not signed off since. A Runner signs off
620
+ * on a clean shutdown (/runner-ingest/disconnect), which is what a helm
621
+ * upgrade or uninstall does to the chart's in-cluster Runner pod — it
622
+ * must read offline at once, not stay "Connected" until its last
623
+ * heartbeat ages out while investigations queue kubectl it will never
624
+ * claim.
625
+ */
626
+ public isRunnerOnline(runner: Runner): boolean {
627
+ return this.getRunnerPresence(runner).isOnline;
628
+ }
629
+
630
+ /*
631
+ * resolveKubernetesAiAccessTarget with each row's liveness read the one
632
+ * way this module reads it.
633
+ */
634
+ public resolveAccessTarget(
635
+ cluster: Pick<KubernetesCluster, "aiAccessRunnerId" | "clusterIdentifier">,
636
+ data: {
637
+ boundRunner: Runner | null | undefined;
638
+ agentRow: KubernetesAiAgent | null | undefined;
639
+ },
640
+ ): KubernetesAiAccessTarget {
641
+ return resolveKubernetesAiAccessTarget({
642
+ clusterIdentifier: cluster.clusterIdentifier,
643
+ aiAccessRunnerId: cluster.aiAccessRunnerId,
644
+ boundRunner: data.boundRunner,
645
+ isBoundRunnerOnline: data.boundRunner
646
+ ? this.isRunnerOnline(data.boundRunner)
647
+ : false,
648
+ agent: data.agentRow,
649
+ isAgentOnline: data.agentRow
650
+ ? KubernetesAiAgentService.isOnline(data.agentRow)
651
+ : false,
652
+ });
653
+ }
654
+
655
+ /*
656
+ * Read what the resolution needs — the bound Runner row and the cluster's
657
+ * agent row (unless the caller already has it) — and resolve. Lookup
658
+ * failures propagate: the enqueue chokepoint must fail closed on a target
659
+ * it could not check.
321
660
  */
322
661
  @CaptureSpan()
323
- public async getStatusForClusterModel(data: {
662
+ public async loadAccessTarget(data: {
324
663
  cluster: KubernetesCluster;
325
- gates: KubernetesClusterAiAccessProjectGates;
326
- }): Promise<KubernetesClusterAiAccessStatus> {
327
- const { cluster, gates } = data;
328
- const gaps: Array<KubernetesAiAccessGap> = [];
664
+ // The cluster's agent row when already read (a batch); undefined reads it.
665
+ agentRow?: KubernetesAiAgent | null | undefined;
666
+ }): Promise<LoadedKubernetesAiAccessTarget> {
667
+ const { cluster } = data;
329
668
 
330
- const remediationMode: KubernetesAiRemediationMode =
331
- this.normalizeRemediationMode(cluster.aiRemediationMode);
332
- const isInvestigationEnabled: boolean =
333
- cluster.isAiInvestigationEnabled === true;
669
+ if (!cluster.id || !cluster.projectId) {
670
+ throw new BadDataException(
671
+ "The cluster's id and project are needed to resolve its AI access target.",
672
+ );
673
+ }
334
674
 
335
- let runnerSummary: KubernetesClusterAiAccessStatus["runner"] = null;
336
- let accessMethod: KubernetesClusterAiAccessStatus["accessMethod"] = "none";
337
- let credentialId: string | undefined = undefined;
338
- let credentialName: string | undefined = undefined;
675
+ const agent: KubernetesAiAgent | null =
676
+ data.agentRow !== undefined
677
+ ? data.agentRow
678
+ : await KubernetesAiAgentService.findForCluster({
679
+ projectId: cluster.projectId,
680
+ kubernetesClusterId: cluster.id,
681
+ });
339
682
 
340
- if (!cluster.aiAccessRunnerId) {
341
- gaps.push(await this.getNoRunnerBoundGap(cluster));
342
- } else {
343
- const runner: Runner | null = await RunnerService.findOneBy({
683
+ const boundRunner: Runner | null = cluster.aiAccessRunnerId
684
+ ? await RunnerService.findOneBy({
685
+ query: {
686
+ _id: cluster.aiAccessRunnerId.toString(),
687
+ projectId: cluster.projectId,
688
+ },
689
+ select: BOUND_RUNNER_SELECT,
690
+ props: { isRoot: true },
691
+ })
692
+ : null;
693
+
694
+ return {
695
+ target: this.resolveAccessTarget(cluster, {
696
+ boundRunner,
697
+ agentRow: agent,
698
+ }),
699
+ boundRunner,
700
+ agent,
701
+ };
702
+ }
703
+
704
+ /*
705
+ * This cluster's previous in-cluster Runner — the kubernetes-agent Runner
706
+ * row an older chart registered — or null. Found by its (bounded) name,
707
+ * matched case-insensitively like registration matches it; a row whose
708
+ * posture names a DIFFERENT cluster is not this cluster's (a shortened
709
+ * name's hash collision). Failing that, the cluster's bound Runner when it
710
+ * is this cluster's agent by posture.
711
+ *
712
+ * A failed lookup propagates rather than reading as none: the AI agent's
713
+ * registration asks this to keep a live Runner's cluster, and "could not
714
+ * tell" must refuse (the agent retries) — never admit an agent that then
715
+ * outranks a Runner that is still working. A caller that may treat "could
716
+ * not tell" as "nothing to do" catches it itself.
717
+ */
718
+ @CaptureSpan()
719
+ public async getLegacyAgentRunnerForCluster(data: {
720
+ projectId: ObjectID;
721
+ kubernetesClusterId: ObjectID;
722
+ clusterIdentifier: string | undefined;
723
+ }): Promise<Runner | null> {
724
+ const clusterIdentifier: string = (data.clusterIdentifier || "").trim();
725
+
726
+ if (!clusterIdentifier) {
727
+ return null;
728
+ }
729
+
730
+ const byName: Runner | null = await RunnerService.findOneBy({
731
+ query: {
732
+ projectId: data.projectId,
733
+ name: QueryHelper.findWithSameText(
734
+ getKubernetesAgentRunnerNameForCluster(clusterIdentifier),
735
+ ),
736
+ },
737
+ select: BOUND_RUNNER_SELECT,
738
+ props: { isRoot: true },
739
+ });
740
+
741
+ if (byName) {
742
+ return this.postureNamesAnotherCluster({
743
+ runner: byName,
744
+ clusterIdentifier,
745
+ })
746
+ ? null
747
+ : byName;
748
+ }
749
+
750
+ const cluster: KubernetesCluster | null =
751
+ await KubernetesClusterService.findOneBy({
344
752
  query: {
345
- _id: cluster.aiAccessRunnerId.toString(),
346
- projectId: cluster.projectId!,
347
- },
348
- select: {
349
- _id: true,
350
- name: true,
351
- lastAlive: true,
352
- connectionStatus: true,
353
- canRunAiCommands: true,
354
- // What would stop an offline agent row from re-registering.
355
- canRunRunbooks: true,
356
- canRunCodeFixTasks: true,
357
- hostInfo: true,
753
+ _id: data.kubernetesClusterId.toString(),
754
+ projectId: data.projectId,
358
755
  },
756
+ select: { _id: true, aiAccessRunnerId: true },
359
757
  props: { isRoot: true },
360
758
  });
361
759
 
362
- if (!runner) {
363
- /*
364
- * Only a race reaches this: the binding's foreign key is ON DELETE
365
- * SET NULL, so deleting the bound Runner clears aiAccessRunnerId in
366
- * the same statement, and the next read takes the no_runner_bound
367
- * branch above. This is the window between reading the cluster row
368
- * and reading its Runner while a delete lands.
369
- */
370
- gaps.push({
371
- code: "runner_missing",
372
- title: "The bound Runner was just deleted",
373
- description:
374
- "The Runner this cluster was bound to was deleted while its status was being read.",
375
- nextStep:
376
- "Reload the cluster's AI page, then bind another Runner there, or reinstall the in-cluster Runner with --set aiAccess.enabled=true.",
377
- blocks: "both",
378
- });
379
- } else {
380
- const posture: KubernetesRunnerPosture | undefined =
381
- parseKubernetesRunnerPosture(runner.hostInfo);
382
- const presence: RunnerPresence = this.getRunnerPresence(runner);
383
- const isOnline: boolean = presence.isOnline;
384
- // The one "is an agent row" rule: the name marker or an agent posture.
385
- const isAgentRunnerRow: boolean =
386
- RunnerServiceClass.isKubernetesAgentRunnerRow(runner);
387
- const isAgentRunner: boolean =
388
- isAgentRunnerRow || posture?.inCluster === true;
389
-
390
- runnerSummary = {
391
- id: runner.id!.toString(),
392
- name: runner.name || "Runner",
393
- isOnline,
394
- lastAliveAt: runner.lastAlive
395
- ? OneUptimeDate.toString(runner.lastAlive)
396
- : undefined,
397
- canRunAiCommands: runner.canRunAiCommands === true,
398
- posture,
399
- };
760
+ if (!cluster?.aiAccessRunnerId) {
761
+ return null;
762
+ }
400
763
 
401
- if (!isOnline) {
402
- /*
403
- * An offline agent Runner of THIS cluster that holds more than the
404
- * agent defaults will be refused on every re-registration its
405
- * restarted pod attempts (it cannot present the key it held), so
406
- * "it reconnects within a minute" would be false: say what blocks
407
- * it instead.
408
- */
409
- const holdingsGap: KubernetesAiAccessGap | null =
410
- RunnerServiceClass.isKubernetesAgentRunnerOfCluster(
411
- runner,
412
- cluster.clusterIdentifier,
413
- )
414
- ? await this.getAgentRunnerHoldingsGap({ runner, cluster })
415
- : null;
416
-
417
- gaps.push(
418
- holdingsGap ||
419
- this.getRunnerOfflineGap({ runner, presence, isAgentRunner }),
420
- );
421
- }
764
+ const bound: Runner | null = await RunnerService.findOneBy({
765
+ query: {
766
+ _id: cluster.aiAccessRunnerId.toString(),
767
+ projectId: data.projectId,
768
+ },
769
+ select: BOUND_RUNNER_SELECT,
770
+ props: { isRoot: true },
771
+ });
422
772
 
423
- if (runner.canRunAiCommands !== true) {
424
- gaps.push({
425
- code: "runner_ai_commands_disabled",
426
- title: "The Runner does not accept AI commands",
427
- description: `"Runs AI Remediation Commands" is turned off on Runner "${runner.name}", so it will not be served kubectl work.`,
428
- nextStep:
429
- 'Turn on "Runs AI Remediation Commands" on the Runner (Project Settings → Runners).',
430
- blocks: "both",
431
- });
432
- }
773
+ return bound &&
774
+ RunnerServiceClass.isKubernetesAgentRunnerOfCluster(
775
+ bound,
776
+ clusterIdentifier,
777
+ )
778
+ ? bound
779
+ : null;
780
+ }
433
781
 
434
- /*
435
- * In-cluster access means "kubectl with the pod's own
436
- * ServiceAccount", which reaches whatever cluster the pod lives
437
- * in. So it is usable only when the Runner's posture names THIS
438
- * cluster — a Runner that is in-cluster somewhere else (the
439
- * dashboard lets an operator pick any Runner) would run every
440
- * command for this cluster against the wrong one.
441
- */
442
- const isInClusterForThisCluster: boolean = isInClusterPostureForCluster(
443
- posture,
444
- cluster.clusterIdentifier,
445
- );
782
+ /*
783
+ * The readiness computation proper. Resolves the cluster's access target
784
+ * (resolveKubernetesAiAccessTarget), reads the bound Runner, the agent row
785
+ * and the credential as root (the cluster row is already tenant-scoped by
786
+ * the caller) and folds project gates in. Always sets aiAgent (the
787
+ * cluster's agent whether or not it is the target) and
788
+ * automaticInvestigation.
789
+ */
790
+ @CaptureSpan()
791
+ public async getStatusForClusterModel(data: {
792
+ cluster: KubernetesCluster;
793
+ gates: KubernetesClusterAiAccessProjectGates;
794
+ /*
795
+ * The cluster's KubernetesAiAgent row when the caller already read it
796
+ * (getStatusesForSubject reads every cluster's in one query); null for
797
+ * "none", undefined to read it here.
798
+ */
799
+ aiAgentRow?: KubernetesAiAgent | null | undefined;
800
+ }): Promise<KubernetesClusterAiAccessStatus> {
801
+ const { cluster, gates } = data;
802
+ const gaps: Array<KubernetesAiAccessGap> = [];
446
803
 
447
- if (isInClusterForThisCluster) {
448
- accessMethod = "in_cluster";
449
- } else if (cluster.aiAccessCredentialId && isAgentRunnerRow) {
450
- /*
451
- * A kubernetes-agent Runner is never handed credential material:
452
- * its row is minted and re-keyed with the project's telemetry
453
- * ingestion key, so a credential it carried would be one leaked
454
- * ingestion key away from anyone. The claim path refuses to
455
- * resolve one for it (by the same name-or-posture rule);
456
- * readiness says so instead of promising access that would fail
457
- * on every command.
458
- */
459
- gaps.push({
460
- code: "credential_on_agent_runner",
461
- title: "An in-cluster Runner cannot carry a credential",
462
- description: `Runner "${runner.name}" is ${
463
- posture?.clusterIdentifier?.trim()
464
- ? `the in-cluster Runner of cluster "${posture.clusterIdentifier.trim()}"`
465
- : "a Kubernetes agent's in-cluster Runner"
466
- }. It runs kubectl with its own ServiceAccount only and is never given a credential, so the Kubernetes credential selected for this cluster cannot be used through it.`,
467
- nextStep:
468
- "Create a Runner under Project Settings → Runners, assign the Kubernetes credential to it and select both on the cluster's AI page — or install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true, which needs no credential.",
469
- blocks: "both",
470
- });
471
- } else if (cluster.aiAccessCredentialId) {
472
- const credential: RunbookCredential | null =
473
- await RunbookCredentialService.findOneBy({
474
- query: {
475
- _id: cluster.aiAccessCredentialId.toString(),
476
- projectId: cluster.projectId!,
477
- },
478
- select: {
479
- _id: true,
480
- name: true,
481
- credentialType: true,
482
- runners: { _id: true },
483
- },
484
- props: { isRoot: true },
485
- });
486
-
487
- const isAssignedToRunner: boolean = Boolean(
488
- credential?.runners?.some((assigned: Runner) => {
489
- return assigned.id?.toString() === runner.id?.toString();
490
- }),
491
- );
804
+ const remediationMode: KubernetesAiRemediationMode =
805
+ this.normalizeRemediationMode(cluster.aiRemediationMode);
806
+ const isInvestigationEnabled: boolean =
807
+ cluster.isAiInvestigationEnabled === true;
492
808
 
493
- if (
494
- !credential ||
495
- credential.credentialType !== RunbookCredentialType.Kubernetes ||
496
- !isAssignedToRunner
497
- ) {
498
- gaps.push({
499
- code: "credential_missing",
500
- title: "The Kubernetes credential is not usable by the Runner",
501
- description: !credential
502
- ? "The credential bound to this cluster no longer exists."
503
- : credential.credentialType !== RunbookCredentialType.Kubernetes
504
- ? `"${credential.name}" is not a Kubernetes credential.`
505
- : `"${credential.name}" is not assigned to Runner "${runner.name}".`,
506
- nextStep:
507
- "Create a Kubernetes credential (API server URL + ServiceAccount token) under Project Settings → Runner Credentials, assign it to the Runner, and select it on the cluster's AI page.",
508
- blocks: "both",
509
- });
510
- } else {
511
- accessMethod = "credential";
512
- credentialId = credential.id!.toString();
513
- credentialName = credential.name;
514
- }
515
- } else if (posture?.inCluster) {
516
- /*
517
- * In-cluster, but for another cluster (or for no named cluster
518
- * at all). Say so explicitly: the operator picked this Runner
519
- * believing "in-cluster" meant "in this cluster".
520
- */
521
- const reportedCluster: string | undefined =
522
- posture.clusterIdentifier?.trim() || undefined;
523
-
524
- gaps.push({
525
- code: "runner_cluster_mismatch",
526
- title: reportedCluster
527
- ? "The bound Runner is the in-cluster Runner of a different cluster"
528
- : "The bound Runner did not report which cluster it runs in",
529
- description: reportedCluster
530
- ? `Runner "${runner.name}" runs inside cluster "${reportedCluster}", not "${
531
- cluster.clusterIdentifier || cluster.name || "this cluster"
532
- }". Its ServiceAccount would run every kubectl command against "${reportedCluster}", so OneUptime AI will not use it for this cluster.`
533
- : `Runner "${runner.name}" reports that it runs inside a Kubernetes cluster but not which one, so OneUptime AI cannot tell whether that is this cluster.`,
534
- nextStep:
535
- "Install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true and select it on the cluster's AI page as its Runner (a registering Runner never replaces the Runner a cluster is bound to), or bind a Runner that holds a Kubernetes credential for this cluster and select that credential on the cluster's AI page.",
536
- blocks: "both",
537
- });
538
- } else {
539
- gaps.push({
540
- code: "credential_missing",
541
- title: "The Runner has no credential for this cluster",
542
- description: `Runner "${runner.name}" runs outside the cluster, so it needs a Kubernetes credential to reach the API server.`,
543
- nextStep:
544
- "Select a Kubernetes credential assigned to this Runner on the cluster's AI page — or install the in-cluster Runner with --set aiAccess.enabled=true, which needs no credential.",
545
- blocks: "both",
546
- });
547
- }
809
+ const agentRow: KubernetesAiAgent | null =
810
+ data.aiAgentRow !== undefined
811
+ ? data.aiAgentRow
812
+ : await this.findAiAgentRowForStatus(cluster);
548
813
 
549
- if (
550
- remediationMode !== KubernetesAiRemediationMode.Disabled &&
551
- posture?.inCluster &&
552
- posture.allowWrites !== true
553
- ) {
554
- gaps.push({
555
- code: "remediation_write_access_missing",
556
- title: "The in-cluster Runner is read-only",
557
- description:
558
- "The Kubernetes agent was installed without write access, so kubectl changes would be refused by the cluster.",
559
- nextStep: REMEDIATION_WRITE_ACCESS_NEXT_STEP,
560
- blocks: "remediation",
561
- });
562
- }
563
- }
564
- }
814
+ const loaded: LoadedKubernetesAiAccessTarget = await this.loadAccessTarget({
815
+ cluster,
816
+ agentRow,
817
+ });
818
+
819
+ const access: ResolvedTargetAccess = await this.getTargetAccess({
820
+ cluster,
821
+ loaded,
822
+ remediationMode,
823
+ });
824
+
825
+ gaps.push(...access.gaps);
565
826
 
566
827
  if (!isInvestigationEnabled) {
567
828
  gaps.push({
@@ -569,8 +830,7 @@ class KubernetesClusterAiAccessServiceClass {
569
830
  title: "AI investigation is turned off for this cluster",
570
831
  description:
571
832
  "OneUptime AI will investigate incidents and alerts on this cluster with OneUptime data only — it will not run kubectl.",
572
- nextStep:
573
- 'Turn on "Let AI investigate with kubectl" on the cluster\'s AI page.',
833
+ nextStep: `Turn on "Investigate with kubectl" on ${CLUSTER_AI_AGENT_PAGE}.`,
574
834
  blocks: "investigation",
575
835
  });
576
836
  }
@@ -578,11 +838,10 @@ class KubernetesClusterAiAccessServiceClass {
578
838
  if (remediationMode === KubernetesAiRemediationMode.Disabled) {
579
839
  gaps.push({
580
840
  code: "remediation_disabled",
581
- title: "AI remediation is turned off for this cluster",
841
+ title: "AI fixes are turned off for this cluster",
582
842
  description:
583
843
  "OneUptime AI will diagnose but never propose or apply a fix on this cluster.",
584
- nextStep:
585
- 'Set "AI remediation" to "Ask for approval", "Automatic" or "Bypass approval" on the cluster\'s AI page.',
844
+ nextStep: `Set "Fixes" to "Ask for approval", "Automatic" or "Bypass approval" on ${CLUSTER_AI_AGENT_PAGE}.`,
586
845
  blocks: "remediation",
587
846
  });
588
847
  }
@@ -592,7 +851,7 @@ class KubernetesClusterAiAccessServiceClass {
592
851
  code: "project_ai_disabled",
593
852
  title: "AI is disabled for this project",
594
853
  description: "OneUptime AI is switched off at the project level.",
595
- nextStep: "Enable AI under Project Settings → AI.",
854
+ nextStep: "Enable AI under Project Settings → AI Features.",
596
855
  blocks: "both",
597
856
  });
598
857
  }
@@ -609,25 +868,46 @@ class KubernetesClusterAiAccessServiceClass {
609
868
  });
610
869
  }
611
870
 
871
+ if (gates.aiBalanceBlocker) {
872
+ gaps.push({
873
+ code: "ai_balance_insufficient",
874
+ title: "The project is out of AI credits",
875
+ description: gates.aiBalanceBlocker,
876
+ nextStep: AI_BALANCE_INSUFFICIENT_NEXT_STEP,
877
+ blocks: "both",
878
+ });
879
+ }
880
+
612
881
  if (!gates.isAutoRemediationEnabled) {
613
882
  gaps.push({
614
883
  code: "project_auto_remediation_disabled",
615
884
  title: "Auto-remediation is disabled for this project",
616
885
  description:
617
886
  "The project-level auto-remediation kill switch is off, so no AI fix can be proposed or run.",
618
- nextStep: "Enable auto-remediation under Project Settings → AI.",
887
+ nextStep:
888
+ "Enable auto-remediation under Project Settings → AI Features.",
619
889
  blocks: "remediation",
620
890
  });
621
891
  }
622
892
 
623
- if (!gates.isAiCommandExecutionEnabled) {
893
+ /*
894
+ * The project's command-execution opt-in governs AI-composed commands
895
+ * on Runners. A cluster reached through its Kubernetes AI agent (or the
896
+ * chart's previous in-cluster Runner) is governed by the cluster's own
897
+ * Fixes setting and the chart's write RBAC instead, so the opt-in only
898
+ * gates a cluster reached through an advanced Runner and credential.
899
+ */
900
+ if (
901
+ loaded.target.type === "advanced_runner" &&
902
+ !gates.isAiCommandExecutionEnabled
903
+ ) {
624
904
  gaps.push({
625
905
  code: "project_ai_command_execution_disabled",
626
906
  title: "AI command execution is not enabled for this project",
627
907
  description:
628
- "AI-composed commands (including kubectl fixes) never run in a project that has not opted in.",
908
+ "This cluster is reached through a Runner, and AI-composed commands (kubectl fixes included) never run on a Runner in a project that has not opted in.",
629
909
  nextStep:
630
- 'Turn on "Enable AI Command Execution" under Project Settings → AI.',
910
+ 'Turn on "Enable AI Command Execution" under Project Settings → AI Features.',
631
911
  blocks: "remediation",
632
912
  });
633
913
  }
@@ -647,10 +927,17 @@ class KubernetesClusterAiAccessServiceClass {
647
927
  clusterId: cluster.id!.toString(),
648
928
  clusterName: cluster.name || cluster.clusterIdentifier || "cluster",
649
929
  clusterIdentifier: cluster.clusterIdentifier,
650
- runner: runnerSummary,
651
- accessMethod,
652
- credentialId,
653
- credentialName,
930
+ runner: access.runner,
931
+ accessMethod: access.accessMethod,
932
+ aiAgent: loaded.agent
933
+ ? KubernetesAiAgentService.toSummary(loaded.agent)
934
+ : null,
935
+ automaticInvestigation: {
936
+ incidents: gates.automaticInvestigation?.incidents === true,
937
+ alerts: gates.automaticInvestigation?.alerts === true,
938
+ },
939
+ credentialId: access.credentialId,
940
+ credentialName: access.credentialName,
654
941
  kubectlAllowlist: this.normalizeAllowlist(
655
942
  cluster.aiKubectlCommandAllowlist,
656
943
  ),
@@ -670,19 +957,389 @@ class KubernetesClusterAiAccessServiceClass {
670
957
  }
671
958
 
672
959
  /*
673
- * Is this Runner online right now? One rule for readiness and for
674
- * registration: it heartbeated within the alive window (RunnerLiveStatus)
675
- * AND it has not signed off since. A Runner signs off on a clean shutdown
676
- * (/runner-ingest/disconnect), which is what a helm uninstall or turning
677
- * aiAccess off does to the agent's Runner pod — it must read offline at
678
- * once, not stay "Connected" until its last heartbeat ages out while
679
- * investigations queue kubectl it will never claim. A freshly created
680
- * row is Disconnected with no heartbeat yet: that is "never connected",
681
- * not a sign-off.
960
+ * The cluster's agent row for a status. Never throws: a failed read is
961
+ * logged and reads as "no agent", which can only make the status say
962
+ * less is reachable (or fall back to a bound Runner), never more.
963
+ */
964
+ private async findAiAgentRowForStatus(
965
+ cluster: KubernetesCluster,
966
+ ): Promise<KubernetesAiAgent | null> {
967
+ if (!cluster.id || !cluster.projectId) {
968
+ return null;
969
+ }
970
+
971
+ try {
972
+ return await KubernetesAiAgentService.findForCluster({
973
+ projectId: cluster.projectId,
974
+ kubernetesClusterId: cluster.id,
975
+ });
976
+ } catch (error) {
977
+ logger.error(
978
+ `KubernetesClusterAiAccess: could not read the Kubernetes AI agent of cluster ${cluster.id.toString()}: ${error}`,
979
+ );
980
+ return null;
981
+ }
982
+ }
983
+
984
+ // How the resolved target reaches the cluster, and what stops it.
985
+ private async getTargetAccess(data: {
986
+ cluster: KubernetesCluster;
987
+ loaded: LoadedKubernetesAiAccessTarget;
988
+ remediationMode: KubernetesAiRemediationMode;
989
+ }): Promise<ResolvedTargetAccess> {
990
+ const { cluster, loaded, remediationMode } = data;
991
+ const target: KubernetesAiAccessTarget = loaded.target;
992
+
993
+ switch (target.type) {
994
+ case "ai_agent":
995
+ return this.getAgentAccess({
996
+ cluster,
997
+ agent: target.agent,
998
+ isOnline: target.isOnline,
999
+ remediationMode,
1000
+ });
1001
+ case "legacy_runner":
1002
+ case "advanced_runner":
1003
+ return this.getRunnerAccess({
1004
+ cluster,
1005
+ runner: target.runner,
1006
+ isLegacy: target.type === "legacy_runner",
1007
+ remediationMode,
1008
+ });
1009
+ default:
1010
+ return {
1011
+ runner: null,
1012
+ accessMethod: "none",
1013
+ gaps: [
1014
+ /*
1015
+ * A bound id with no Runner row is only the race with a Runner
1016
+ * delete: the binding's foreign key is ON DELETE SET NULL, so
1017
+ * the next read has no binding at all and reads as not
1018
+ * connected.
1019
+ */
1020
+ cluster.aiAccessRunnerId && !loaded.boundRunner
1021
+ ? {
1022
+ code: "runner_missing",
1023
+ title: "The bound Runner was just deleted",
1024
+ description:
1025
+ "The Runner this cluster was bound to was deleted while its status was being read.",
1026
+ nextStep: `Reload ${CLUSTER_AI_AGENT_PAGE}.`,
1027
+ blocks: "both",
1028
+ }
1029
+ : {
1030
+ code: "ai_agent_not_connected",
1031
+ title: "The Kubernetes AI agent is not connected",
1032
+ description: `OneUptime AI runs kubectl on this cluster through the Kubernetes AI agent, and no agent has connected for this cluster yet.${this.getIgnoredBoundRunnerNote(
1033
+ { cluster, boundRunner: loaded.boundRunner },
1034
+ )}`,
1035
+ nextStep: AI_AGENT_NOT_CONNECTED_NEXT_STEP,
1036
+ blocks: "both",
1037
+ },
1038
+ ],
1039
+ };
1040
+ }
1041
+ }
1042
+
1043
+ /*
1044
+ * A Runner still bound to a cluster that has no access target is another
1045
+ * cluster's in-cluster Runner (a kubernetes-agent row that is not this
1046
+ * cluster's): its ServiceAccount can never reach this cluster, so it is
1047
+ * ignored. Said, so the status does not read as if nothing were
1048
+ * selected; installing the agent (the gap's step) is still the fix.
1049
+ * Empty when nothing is bound.
1050
+ */
1051
+ private getIgnoredBoundRunnerNote(data: {
1052
+ cluster: KubernetesCluster;
1053
+ boundRunner: Runner | null;
1054
+ }): string {
1055
+ const { cluster, boundRunner } = data;
1056
+
1057
+ if (!cluster.aiAccessRunnerId || !boundRunner) {
1058
+ return "";
1059
+ }
1060
+
1061
+ const reportedCluster: string | undefined =
1062
+ parseKubernetesRunnerPosture(
1063
+ boundRunner.hostInfo,
1064
+ )?.clusterIdentifier?.trim() || undefined;
1065
+
1066
+ return ` Runner "${boundRunner.name || "Runner"}" is still selected for this cluster, but it is not this cluster's in-cluster Runner${
1067
+ reportedCluster ? ` (it reports cluster "${reportedCluster}")` : ""
1068
+ }, so OneUptime AI does not use it.`;
1069
+ }
1070
+
1071
+ /*
1072
+ * The cluster's Kubernetes AI agent as the target. It runs kubectl with
1073
+ * its own ServiceAccount (accessMethod "in_cluster"), always accepts AI
1074
+ * commands and never carries a credential.
1075
+ */
1076
+ private getAgentAccess(data: {
1077
+ cluster: KubernetesCluster;
1078
+ agent: KubernetesAiAgent;
1079
+ isOnline: boolean;
1080
+ remediationMode: KubernetesAiRemediationMode;
1081
+ }): ResolvedTargetAccess {
1082
+ const { cluster, agent, isOnline, remediationMode } = data;
1083
+ const gaps: Array<KubernetesAiAccessGap> = [];
1084
+ const posture: KubernetesAgentPosture | undefined =
1085
+ parseKubernetesAgentPosture(agent.posture);
1086
+
1087
+ const runner: KubernetesAiAccessRunnerSummary = {
1088
+ id: agent.id!.toString(),
1089
+ name: KUBERNETES_AI_AGENT_DISPLAY_NAME,
1090
+ kind: "ai_agent",
1091
+ isOnline,
1092
+ lastAliveAt: agent.lastAliveAt
1093
+ ? OneUptimeDate.toString(agent.lastAliveAt)
1094
+ : undefined,
1095
+ canRunAiCommands: true,
1096
+ posture,
1097
+ };
1098
+
1099
+ if (!isOnline) {
1100
+ const lastAliveText: string = agent.lastAliveAt
1101
+ ? OneUptimeDate.getDateAsFormattedString(agent.lastAliveAt)
1102
+ : "an unknown time";
1103
+
1104
+ gaps.push({
1105
+ code: "ai_agent_offline",
1106
+ title: "The Kubernetes AI agent is offline",
1107
+ description:
1108
+ agent.connectionStatus === "disconnected"
1109
+ ? `The Kubernetes AI agent signed off after its last report at ${lastAliveText}: it was stopped, uninstalled, turned off with aiAgent.enabled=false, or reset, or its pod is being replaced.`
1110
+ : `The Kubernetes AI agent last reported in at ${lastAliveText}.`,
1111
+ nextStep: `Check the agent's pod: ${getAiAgentLogsCommand(
1112
+ posture?.podNamespace,
1113
+ )}. It must be able to reach your OneUptime URL.`,
1114
+ blocks: "both",
1115
+ });
1116
+ }
1117
+
1118
+ /*
1119
+ * The server stamps the cluster's identifier on the agent's posture
1120
+ * every time it reports, so this only fails for a cluster row without
1121
+ * an identifier. The enqueue chokepoint refuses the same case, so the
1122
+ * status must not call it ready.
1123
+ */
1124
+ const isInClusterForThisCluster: boolean = isInClusterPostureForCluster(
1125
+ posture,
1126
+ cluster.clusterIdentifier,
1127
+ );
1128
+
1129
+ if (isOnline && !isInClusterForThisCluster) {
1130
+ gaps.push({
1131
+ code: "runner_cluster_mismatch",
1132
+ title: "The Kubernetes AI agent has not reported this cluster",
1133
+ description:
1134
+ "The agent's last report does not name this cluster, so OneUptime AI does not use it yet.",
1135
+ nextStep: `Reset the agent on ${CLUSTER_AI_AGENT_PAGE}; it reconnects on its own within a few minutes.`,
1136
+ blocks: "both",
1137
+ });
1138
+ }
1139
+
1140
+ if (
1141
+ remediationMode !== KubernetesAiRemediationMode.Disabled &&
1142
+ posture?.allowWrites !== true
1143
+ ) {
1144
+ gaps.push({
1145
+ code: "remediation_write_access_missing",
1146
+ title: "The Kubernetes AI agent is read-only",
1147
+ description:
1148
+ "The Kubernetes AI agent was installed without write access, so it cannot apply fixes.",
1149
+ nextStep: getRemediationWriteAccessNextStep("ai_agent"),
1150
+ blocks: "remediation",
1151
+ });
1152
+ }
1153
+
1154
+ return {
1155
+ runner,
1156
+ accessMethod: isInClusterForThisCluster ? "in_cluster" : "none",
1157
+ gaps,
1158
+ };
1159
+ }
1160
+
1161
+ /*
1162
+ * A Runner as the target: the chart's previous in-cluster Runner of this
1163
+ * cluster (isLegacy), or a Runner an operator bound, which reaches the
1164
+ * cluster with the cluster's Kubernetes credential.
682
1165
  */
1166
+ private async getRunnerAccess(data: {
1167
+ cluster: KubernetesCluster;
1168
+ runner: Runner;
1169
+ isLegacy: boolean;
1170
+ remediationMode: KubernetesAiRemediationMode;
1171
+ }): Promise<ResolvedTargetAccess> {
1172
+ const { cluster, runner, isLegacy, remediationMode } = data;
1173
+ const gaps: Array<KubernetesAiAccessGap> = [];
1174
+ const posture: KubernetesRunnerPosture | undefined =
1175
+ parseKubernetesRunnerPosture(runner.hostInfo);
1176
+ const presence: RunnerPresence = this.getRunnerPresence(runner);
1177
+
1178
+ let accessMethod: KubernetesClusterAiAccessStatus["accessMethod"] = "none";
1179
+ let credentialId: string | undefined = undefined;
1180
+ let credentialName: string | undefined = undefined;
1181
+
1182
+ const summary: KubernetesAiAccessRunnerSummary = {
1183
+ id: runner.id!.toString(),
1184
+ name: runner.name || "Runner",
1185
+ kind: "runner",
1186
+ isOnline: presence.isOnline,
1187
+ lastAliveAt: runner.lastAlive
1188
+ ? OneUptimeDate.toString(runner.lastAlive)
1189
+ : undefined,
1190
+ canRunAiCommands: runner.canRunAiCommands === true,
1191
+ posture,
1192
+ };
1193
+
1194
+ if (!presence.isOnline) {
1195
+ gaps.push(this.getRunnerOfflineGap({ runner, presence, isLegacy }));
1196
+ }
1197
+
1198
+ if (runner.canRunAiCommands !== true) {
1199
+ gaps.push({
1200
+ code: "runner_ai_commands_disabled",
1201
+ title: "The Runner does not accept AI commands",
1202
+ description: `"Runs AI Remediation Commands" is turned off on Runner "${runner.name}", so it will not be served kubectl work.`,
1203
+ nextStep:
1204
+ 'Turn on "Runs AI Remediation Commands" on the Runner (Project Settings → Runners).',
1205
+ blocks: "both",
1206
+ });
1207
+ }
1208
+
1209
+ /*
1210
+ * In-cluster access means "kubectl with the pod's own ServiceAccount",
1211
+ * which reaches whatever cluster the pod lives in. So it is usable only
1212
+ * when the Runner's posture names THIS cluster — a Runner that is
1213
+ * in-cluster somewhere else would run every command for this cluster
1214
+ * against the wrong one. The previous in-cluster Runner of this cluster
1215
+ * ignores a credential still selected: in-cluster access wins.
1216
+ */
1217
+ const isInClusterForThisCluster: boolean = isInClusterPostureForCluster(
1218
+ posture,
1219
+ cluster.clusterIdentifier,
1220
+ );
1221
+
1222
+ if (isInClusterForThisCluster) {
1223
+ accessMethod = "in_cluster";
1224
+ } else if (isLegacy) {
1225
+ /*
1226
+ * This cluster's previous in-cluster Runner by its name, but its last
1227
+ * report does not say it runs here (a heartbeat that dropped its
1228
+ * posture). It is never given a credential, so it cannot be used —
1229
+ * and the AI agent, which needs neither, is the way forward.
1230
+ */
1231
+ gaps.push({
1232
+ code: "runner_cluster_mismatch",
1233
+ title: "The previous in-cluster Runner has not reported this cluster",
1234
+ description: `Runner "${runner.name}" has not reported that it runs in this cluster, so OneUptime AI does not use it.`,
1235
+ nextStep: `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over.\n\n${AI_AGENT_INSTALL_COMMAND}`,
1236
+ blocks: "both",
1237
+ });
1238
+ } else if (cluster.aiAccessCredentialId) {
1239
+ const credential: RunbookCredential | null =
1240
+ await RunbookCredentialService.findOneBy({
1241
+ query: {
1242
+ _id: cluster.aiAccessCredentialId.toString(),
1243
+ projectId: cluster.projectId!,
1244
+ },
1245
+ select: {
1246
+ _id: true,
1247
+ name: true,
1248
+ credentialType: true,
1249
+ runners: { _id: true },
1250
+ },
1251
+ props: { isRoot: true },
1252
+ });
1253
+
1254
+ const isAssignedToRunner: boolean = Boolean(
1255
+ credential?.runners?.some((assigned: Runner) => {
1256
+ return assigned.id?.toString() === runner.id?.toString();
1257
+ }),
1258
+ );
1259
+
1260
+ if (
1261
+ !credential ||
1262
+ credential.credentialType !== RunbookCredentialType.Kubernetes ||
1263
+ !isAssignedToRunner
1264
+ ) {
1265
+ gaps.push({
1266
+ code: "credential_missing",
1267
+ title: "The Kubernetes credential is not usable by the Runner",
1268
+ description: !credential
1269
+ ? "The credential bound to this cluster no longer exists."
1270
+ : credential.credentialType !== RunbookCredentialType.Kubernetes
1271
+ ? `"${credential.name}" is not a Kubernetes credential.`
1272
+ : `"${credential.name}" is not assigned to Runner "${runner.name}".`,
1273
+ nextStep: `Create a Kubernetes credential (API server URL + ServiceAccount token) under Project Settings → Runner Credentials, assign it to the Runner, and select it on ${CLUSTER_AI_AGENT_PAGE}.`,
1274
+ blocks: "both",
1275
+ });
1276
+ } else {
1277
+ accessMethod = "credential";
1278
+ credentialId = credential.id!.toString();
1279
+ credentialName = credential.name;
1280
+ }
1281
+ } else if (posture?.inCluster) {
1282
+ /*
1283
+ * In-cluster, but for no named cluster. Say so explicitly: the
1284
+ * operator picked this Runner believing "in-cluster" meant "in this
1285
+ * cluster". (A Runner that names a cluster reports an agent posture,
1286
+ * so it is a kubernetes-agent row and never reaches this branch:
1287
+ * another cluster's is no target at all — see
1288
+ * getIgnoredBoundRunnerNote.)
1289
+ */
1290
+ gaps.push({
1291
+ code: "runner_cluster_mismatch",
1292
+ title: "The bound Runner did not report which cluster it runs in",
1293
+ description: `Runner "${runner.name}" reports that it runs inside a Kubernetes cluster but not which one, so OneUptime AI cannot tell whether that is this cluster.`,
1294
+ nextStep: `Clear the Runner on ${CLUSTER_AI_AGENT_PAGE} to use this cluster's Kubernetes AI agent, or select a Kubernetes credential for this cluster that is assigned to the Runner.`,
1295
+ blocks: "both",
1296
+ });
1297
+ } else {
1298
+ gaps.push({
1299
+ code: "credential_missing",
1300
+ title: "The Runner has no credential for this cluster",
1301
+ description: `Runner "${runner.name}" runs outside the cluster, so it needs a Kubernetes credential to reach the API server.`,
1302
+ nextStep: `Select a Kubernetes credential assigned to this Runner on ${CLUSTER_AI_AGENT_PAGE} — or clear the Runner there to use the Kubernetes AI agent, which needs no credential.`,
1303
+ blocks: "both",
1304
+ });
1305
+ }
1306
+
1307
+ if (
1308
+ remediationMode !== KubernetesAiRemediationMode.Disabled &&
1309
+ posture?.inCluster &&
1310
+ posture.allowWrites !== true
1311
+ ) {
1312
+ gaps.push({
1313
+ code: "remediation_write_access_missing",
1314
+ title: isLegacy
1315
+ ? "The previous in-cluster Runner is read-only"
1316
+ : "The Runner is read-only",
1317
+ description: isLegacy
1318
+ ? "The Kubernetes agent was installed without write access, so kubectl changes would be refused by the cluster."
1319
+ : `Runner "${runner.name}" reports that it may not change the cluster, so kubectl changes would be refused.`,
1320
+ nextStep: getRemediationWriteAccessNextStep(
1321
+ isLegacy ? "legacy_runner" : "credential_runner",
1322
+ ),
1323
+ blocks: "remediation",
1324
+ });
1325
+ }
1326
+
1327
+ return {
1328
+ runner: summary,
1329
+ accessMethod,
1330
+ credentialId,
1331
+ credentialName,
1332
+ gaps,
1333
+ };
1334
+ }
1335
+
683
1336
  private getRunnerPresence(runner: Runner): RunnerPresence {
684
1337
  const liveStatus: RunnerLiveStatus = getRunnerLiveStatus(runner.lastAlive);
685
1338
 
1339
+ /*
1340
+ * A freshly created row is Disconnected with no heartbeat yet: that is
1341
+ * "never connected", not a sign-off.
1342
+ */
686
1343
  const isSignedOff: boolean =
687
1344
  runner.connectionStatus === RunnerConnectionStatus.Disconnected &&
688
1345
  liveStatus !== RunnerLiveStatus.NeverConnected;
@@ -697,26 +1354,39 @@ class KubernetesClusterAiAccessServiceClass {
697
1354
  private getRunnerOfflineGap(data: {
698
1355
  runner: Runner;
699
1356
  presence: RunnerPresence;
700
- // The Runner is a Kubernetes agent's in-cluster Runner.
701
- isAgentRunner: boolean;
1357
+ // The Runner is this cluster's previous in-cluster Runner.
1358
+ isLegacy: boolean;
702
1359
  }): KubernetesAiAccessGap {
703
- const { runner, presence, isAgentRunner } = data;
1360
+ const { runner, presence, isLegacy } = data;
704
1361
  const lastAliveText: string = runner.lastAlive
705
1362
  ? OneUptimeDate.getDateAsFormattedString(runner.lastAlive)
706
1363
  : "an unknown time";
707
1364
 
1365
+ /*
1366
+ * The previous in-cluster Runner is not coming back by itself after a
1367
+ * chart upgrade (the new chart does not install it), and there is
1368
+ * nothing to fix in it: the step is the upgrade to the AI agent.
1369
+ */
1370
+ if (isLegacy) {
1371
+ return {
1372
+ code: "runner_offline",
1373
+ title: presence.isSignedOff
1374
+ ? "The previous in-cluster Runner signed off"
1375
+ : "The previous in-cluster Runner is offline",
1376
+ description: presence.isSignedOff
1377
+ ? `Runner "${runner.name}" shut down after its last report at ${lastAliveText}.`
1378
+ : `Runner "${runner.name}" last reported in at ${lastAliveText}.`,
1379
+ nextStep: `Upgrade the Kubernetes agent chart: the Kubernetes AI agent replaces this Runner and your settings carry over.\n\n${AI_AGENT_INSTALL_COMMAND}`,
1380
+ blocks: "both",
1381
+ };
1382
+ }
1383
+
708
1384
  if (presence.isSignedOff) {
709
1385
  return {
710
1386
  code: "runner_offline",
711
- title: isAgentRunner
712
- ? "The in-cluster Runner signed off"
713
- : "The Runner signed off",
714
- description: isAgentRunner
715
- ? `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: the Kubernetes agent was uninstalled or upgraded with aiAccess turned off, or its Runner pod is being replaced.`
716
- : `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: its container was stopped.`,
717
- nextStep: isAgentRunner
718
- ? "If the agent was uninstalled or aiAccess was turned off on purpose, clear the Runner on the cluster's AI page. Otherwise upgrade the agent with --set aiAccess.enabled=true and the Runner reconnects within a minute."
719
- : "Start the Runner container again, or bind another Runner on the cluster's AI page.",
1387
+ title: "The Runner signed off",
1388
+ description: `Runner "${runner.name}" shut down cleanly after its last heartbeat at ${lastAliveText}: its container was stopped.`,
1389
+ nextStep: `Start the Runner container again, or clear the Runner on ${CLUSTER_AI_AGENT_PAGE} to use the Kubernetes AI agent.`,
720
1390
  blocks: "both",
721
1391
  };
722
1392
  }
@@ -732,59 +1402,8 @@ class KubernetesClusterAiAccessServiceClass {
732
1402
  description: isNeverConnected
733
1403
  ? `Runner "${runner.name}" exists but has not reported in yet.`
734
1404
  : `Runner "${runner.name}" last reported in at ${lastAliveText}.`,
735
- nextStep: isAgentRunner
736
- ? "Check the in-cluster Runner pod: kubectl get pods -n <agent-namespace> -l component=ai-runner, then its logs. It must be able to reach your OneUptime URL."
737
- : "Start the Runner container and make sure it can reach your OneUptime URL.",
738
- blocks: "both",
739
- };
740
- }
741
-
742
- /*
743
- * The gap for an offline agent Runner of this cluster that holds more than
744
- * the agent defaults, or null when it holds nothing extra. Such a row is
745
- * refused on every registration that cannot present its current key — and
746
- * a restarted pod never can, it keeps the key in memory only — so the
747
- * Runner will not come back until an operator removes what it holds.
748
- * Never throws: a failed lookup falls back to the ordinary offline gap.
749
- */
750
- private async getAgentRunnerHoldingsGap(data: {
751
- runner: Runner;
752
- cluster: KubernetesCluster;
753
- }): Promise<KubernetesAiAccessGap | null> {
754
- let holdings: Array<AgentRunnerHolding> = [];
755
-
756
- try {
757
- holdings = await this.getRunnerHoldingsBeyondDefaults({
758
- runner: data.runner,
759
- clusterId: data.cluster.id!,
760
- projectId: data.cluster.projectId!,
761
- });
762
- } catch (error) {
763
- logger.error(
764
- `KubernetesClusterAiAccess: could not read what the agent Runner of cluster ${data.cluster.id?.toString()} holds: ${error}`,
765
- );
766
- return null;
767
- }
768
-
769
- if (holdings.length === 0) {
770
- return null;
771
- }
772
-
773
- return {
774
- code: "runner_offline",
775
- title: "The in-cluster Runner cannot re-register until it holds less",
776
- description: `Runner "${data.runner.name}" is offline, and it holds more than an in-cluster Runner's defaults (${this.describeHoldings(
777
- holdings,
778
- )}). A restarted in-cluster Runner cannot prove it is the same Runner, so every registration it attempts is refused and it will not reconnect on its own.`,
779
- /*
780
- * Removing what it holds comes first: the Runner row survives, so the
781
- * cluster stays bound to it and it reconnects on its next retry.
782
- * Deleting it works too, but only with the second step — the fresh
783
- * Runner is not bound to this cluster until someone selects it.
784
- */
785
- nextStep: `Under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(
786
- holdings,
787
- )}. The in-cluster Runner then reconnects on its next retry, still bound to this cluster. Or delete the Runner and, once the in-cluster Runner registers a fresh one (within a minute), select it on the cluster's AI page as its Runner. ${getDeletedAgentRunnerRebindNote()}`,
1405
+ nextStep:
1406
+ "Start the Runner container and make sure it can reach your OneUptime URL.",
788
1407
  blocks: "both",
789
1408
  };
790
1409
  }
@@ -805,95 +1424,6 @@ class KubernetesClusterAiAccessServiceClass {
805
1424
  .join("; ");
806
1425
  }
807
1426
 
808
- /*
809
- * The gap for a cluster with no Runner bound. What to do depends on
810
- * whether this cluster's in-cluster Runner is already registered: a
811
- * registering Runner never binds a cluster whose AI access was configured
812
- * before (an operator cleared the binding, or the Runner it was bound to
813
- * was deleted), so re-running the helm upgrade would change nothing —
814
- * selecting the Runner on the AI page is the step that works. Never
815
- * throws: a failed lookup falls back to the install instruction.
816
- */
817
- private async getNoRunnerBoundGap(
818
- cluster: KubernetesCluster,
819
- ): Promise<KubernetesAiAccessGap> {
820
- const agentRunner: Runner | null =
821
- await this.findAgentRunnerForCluster(cluster);
822
-
823
- if (agentRunner) {
824
- const isOnline: boolean = this.getRunnerPresence(agentRunner).isOnline;
825
-
826
- return {
827
- code: "no_runner_bound",
828
- title: "The in-cluster Runner is installed but not selected",
829
- description: `Runner "${agentRunner.name}", this cluster's in-cluster Runner, is registered${
830
- isOnline ? " and online" : " but not online right now"
831
- }, but no Runner is bound to this cluster, so OneUptime AI does not use it. A registering Runner never re-binds a cluster that had a Runner bound before (or already ran kubectl) — its binding was cleared by an operator, or the Runner it was bound to was deleted.`,
832
- nextStep: `Select the kubernetes-agent Runner "${agentRunner.name}" on the cluster's AI page as its Runner (leave the credential empty). No helm change is needed.`,
833
- blocks: "both",
834
- };
835
- }
836
-
837
- return {
838
- code: "no_runner_bound",
839
- title: "No Runner can reach this cluster",
840
- description:
841
- "OneUptime AI runs kubectl through a Runner. None is bound to this cluster yet.",
842
- nextStep:
843
- "Upgrade the Kubernetes agent with --set aiAccess.enabled=true to install an in-cluster Runner (one command), or bind an existing Runner and a Kubernetes credential on the cluster's AI page.",
844
- blocks: "both",
845
- };
846
- }
847
-
848
- /*
849
- * This cluster's agent Runner row, by its (bounded) name, matched
850
- * case-insensitively like registration matches it. A row whose posture
851
- * names a DIFFERENT cluster is not this cluster's agent (a shortened
852
- * name's hash collision, or a posture rewritten on heartbeat).
853
- */
854
- private async findAgentRunnerForCluster(
855
- cluster: KubernetesCluster,
856
- ): Promise<Runner | null> {
857
- const clusterIdentifier: string = (cluster.clusterIdentifier || "").trim();
858
-
859
- if (!clusterIdentifier || !cluster.projectId) {
860
- return null;
861
- }
862
-
863
- try {
864
- const runner: Runner | null = await RunnerService.findOneBy({
865
- query: {
866
- projectId: cluster.projectId,
867
- name: QueryHelper.findWithSameText(
868
- getKubernetesAgentRunnerNameForCluster(clusterIdentifier),
869
- ),
870
- },
871
- select: {
872
- _id: true,
873
- name: true,
874
- lastAlive: true,
875
- connectionStatus: true,
876
- hostInfo: true,
877
- },
878
- props: { isRoot: true },
879
- });
880
-
881
- if (
882
- !runner ||
883
- this.postureNamesAnotherCluster({ runner, clusterIdentifier })
884
- ) {
885
- return null;
886
- }
887
-
888
- return runner;
889
- } catch (error) {
890
- logger.error(
891
- `KubernetesClusterAiAccess: could not look up the agent Runner of cluster ${cluster.id?.toString()}: ${error}`,
892
- );
893
- return null;
894
- }
895
- }
896
-
897
1427
  // The Runner's reported posture names a cluster, and it is not this one.
898
1428
  private postureNamesAnotherCluster(data: {
899
1429
  runner: Runner;
@@ -1064,10 +1594,56 @@ class KubernetesClusterAiAccessServiceClass {
1064
1594
  const gates: KubernetesClusterAiAccessProjectGates =
1065
1595
  await this.getProjectGates(data.projectId);
1066
1596
 
1597
+ return this.getStatusesForClusters({
1598
+ projectId: data.projectId,
1599
+ clusters,
1600
+ gates,
1601
+ });
1602
+ }
1603
+
1604
+ /*
1605
+ * The status of several clusters of one project, with every cluster's
1606
+ * agent row read in ONE query instead of one per cluster. When that read
1607
+ * fails, each status reads its own row (and treats a failure there as no
1608
+ * agent), exactly as a single status would.
1609
+ */
1610
+ @CaptureSpan()
1611
+ public async getStatusesForClusters(data: {
1612
+ projectId: ObjectID;
1613
+ clusters: Array<KubernetesCluster>;
1614
+ gates: KubernetesClusterAiAccessProjectGates;
1615
+ }): Promise<Array<KubernetesClusterAiAccessStatus>> {
1616
+ let agentRows: Map<string, KubernetesAiAgent> | null = null;
1617
+
1618
+ try {
1619
+ agentRows = await KubernetesAiAgentService.findForClusters({
1620
+ projectId: data.projectId,
1621
+ kubernetesClusterIds: data.clusters
1622
+ .map((cluster: KubernetesCluster): ObjectID | undefined => {
1623
+ return cluster.id || undefined;
1624
+ })
1625
+ .filter((id: ObjectID | undefined): id is ObjectID => {
1626
+ return Boolean(id);
1627
+ }),
1628
+ });
1629
+ } catch (error) {
1630
+ logger.error(
1631
+ `KubernetesClusterAiAccess: could not read the Kubernetes AI agents of project ${data.projectId.toString()}; reading them one cluster at a time: ${error}`,
1632
+ );
1633
+ }
1634
+
1067
1635
  const statuses: Array<KubernetesClusterAiAccessStatus> = [];
1068
1636
 
1069
- for (const cluster of clusters) {
1070
- statuses.push(await this.getStatusForClusterModel({ cluster, gates }));
1637
+ for (const cluster of data.clusters) {
1638
+ statuses.push(
1639
+ await this.getStatusForClusterModel({
1640
+ cluster,
1641
+ gates: data.gates,
1642
+ aiAgentRow: agentRows
1643
+ ? agentRows.get(cluster.id?.toString() || "") || null
1644
+ : undefined,
1645
+ }),
1646
+ );
1071
1647
  }
1072
1648
 
1073
1649
  return statuses;
@@ -1075,12 +1651,14 @@ class KubernetesClusterAiAccessServiceClass {
1075
1651
 
1076
1652
  /*
1077
1653
  * ------------------------------------------------------------------
1078
- * In-cluster Runner registration
1654
+ * Previous in-cluster Runner registration
1079
1655
  * ------------------------------------------------------------------
1080
1656
  */
1081
1657
 
1082
1658
  /*
1083
- * Called by the Runner the kubernetes-agent chart deploys. Authenticated
1659
+ * Called by the in-cluster Runner older kubernetes-agent charts deploy
1660
+ * (the Kubernetes AI agent replaces it and registers through
1661
+ * KubernetesAiAgentService instead). Authenticated
1084
1662
  * by the project's telemetry ingestion key (the same key the agent ships
1085
1663
  * telemetry with), so installing the chart with one extra flag is the
1086
1664
  * whole setup. Idempotent per (project, cluster): a restarted pod gets a
@@ -1111,15 +1689,30 @@ class KubernetesClusterAiAccessServiceClass {
1111
1689
  * secret assigned to it, runbooks or code fixes turned on, or another
1112
1690
  * cluster bound to it — is refused: re-keying it would hand all of that
1113
1691
  * to whoever holds the ingestion key. The operator removes the extras
1114
- * (the Runner then comes back still bound), or deletes the Runner, lets
1115
- * the agent register a fresh one and selects it on the cluster's AI
1116
- * page — a deleted Runner's cluster is left unbound, see below.
1692
+ * (the Runner then comes back still bound), or upgrades the chart to
1693
+ * the Kubernetes AI agent, which needs no Runner at all.
1117
1694
  * - It finds the row it may re-key by this cluster's agent Runner NAME
1118
1695
  * only — never by the posture a Runner reports about itself — and only
1119
1696
  * binds a cluster that never had a Runner bound: a cluster whose
1120
1697
  * binding was cleared (or whose Runner was deleted) stays unbound, and
1121
1698
  * an operator's AI settings are never overwritten, only the chart's
1122
1699
  * defaults fill in a cluster nobody configured.
1700
+ * - While the cluster's Kubernetes AI agent is online the Runner is
1701
+ * refused (superseded_by_ai_agent) before anything is written: the
1702
+ * agent replaces it. The refusal clears on its own once the agent
1703
+ * stops, so a helm rollback to a chart with the Runner works again for
1704
+ * a cluster still bound to that Runner (already_bound — every cluster
1705
+ * the Runner served before the upgrade).
1706
+ * - A cluster that has a Kubernetes AI agent row is never given the
1707
+ * chart's defaults by a Runner's first bind (the agent's own first
1708
+ * connection decides those). The binding rules below are unchanged by
1709
+ * the agent row: a cluster that never had a Runner bound and never ran
1710
+ * kubectl is bound with no switch moved, but one where AI already ran
1711
+ * kubectl — through the agent too: its commands and "Test connection"
1712
+ * record the same history — stays unbound (left_unbound_by_operator).
1713
+ * That history cannot tell the agent's commands from a revoked
1714
+ * Runner's, so it fails closed: nothing reaches the cluster until its
1715
+ * agent is back online.
1123
1716
  * - Every refusal is a KubernetesAgentRegistrationRefusedException whose
1124
1717
  * reason tells the Runner whether waiting helps.
1125
1718
  *
@@ -1177,6 +1770,30 @@ class KubernetesClusterAiAccessServiceClass {
1177
1770
  throw new BadDataException("Cluster could not be resolved.");
1178
1771
  }
1179
1772
 
1773
+ /*
1774
+ * The Kubernetes AI agent replaces this Runner: while the cluster's
1775
+ * agent is online nothing is created, re-keyed or bound. Checked
1776
+ * first, so a rolled-back pod that keeps retrying during an upgrade
1777
+ * never takes the cluster back from a working agent.
1778
+ */
1779
+ const aiAgent: KubernetesAiAgent | null =
1780
+ await KubernetesAiAgentService.findForCluster({
1781
+ projectId: data.projectId,
1782
+ kubernetesClusterId: cluster.id!,
1783
+ });
1784
+
1785
+ if (aiAgent && KubernetesAiAgentService.isOnline(aiAgent)) {
1786
+ logger.warn(
1787
+ `KubernetesClusterAiAccess: refused to register the in-cluster Runner of cluster "${clusterIdentifier}" in project ${data.projectId.toString()}: the cluster's Kubernetes AI agent is online and replaces it.`,
1788
+ );
1789
+
1790
+ throw new KubernetesAgentRegistrationRefusedException({
1791
+ reason: "superseded_by_ai_agent",
1792
+ retryAfterSeconds: SUPERSEDED_BY_AI_AGENT_RETRY_AFTER_SECONDS,
1793
+ message: getSupersededByAiAgentMessage(clusterIdentifier),
1794
+ });
1795
+ }
1796
+
1180
1797
  /*
1181
1798
  * What the row stores: the body's posture, with the cluster and
1182
1799
  * in-cluster set by the service. A re-key may fill in the write scope
@@ -1239,7 +1856,7 @@ class KubernetesClusterAiAccessServiceClass {
1239
1856
 
1240
1857
  throw new KubernetesAgentRegistrationRefusedException({
1241
1858
  reason: "runner_belongs_to_another_cluster",
1242
- message: `Delete Runner "${runner.name}" under Project Settings → Runners (an in-cluster Runner cannot be renamed) and, once the agent registers a fresh one on its next retry, select it on the AI page of cluster "${clusterIdentifier}" as its Runner. That Runner already exists in this project but reports that it is the in-cluster Runner of a different cluster, so it was not reused for cluster "${clusterIdentifier}". ${getDeletedAgentRunnerRebindNote()}`,
1859
+ message: `Upgrade the Kubernetes agent chart of cluster "${clusterIdentifier}": its Kubernetes AI agent replaces the in-cluster Runner and does not need Runner "${runner.name}". That Runner already exists in this project but reports that it is the in-cluster Runner of a different cluster, so it was not reused for cluster "${clusterIdentifier}".`,
1243
1860
  });
1244
1861
  }
1245
1862
 
@@ -1319,13 +1936,21 @@ class KubernetesClusterAiAccessServiceClass {
1319
1936
  * binding's foreign key is ON DELETE SET NULL, so Postgres clears
1320
1937
  * aiAccessRunnerId in the same statement. Deleting the Runner is
1321
1938
  * how an operator revokes or resets it, so it must not come back
1322
- * bound a few heartbeats later. The AI page tells the operator to
1323
- * select the Runner if they want it back.
1939
+ * bound a few heartbeats later. Such a cluster uses its
1940
+ * Kubernetes AI agent instead.
1941
+ * History counts whoever made it: with a Kubernetes AI agent row
1942
+ * it is usually the agent's (it records every command and "Test
1943
+ * connection" on the cluster), but it cannot be told apart from a
1944
+ * revoked Runner's, so such a cluster stays unbound too and is
1945
+ * reached again once its agent is back online. A rollback keeps
1946
+ * working through the Runner a cluster is still bound to
1947
+ * (already_bound above), not through a new binding here.
1324
1948
  * - that never had a Runner bound but was configured on the AI page
1325
1949
  * (aiAccessConfiguredAt: an operator chose a mode, the switches
1326
- * or an allowlist BEFORE installing the chart) is bound to this
1327
- * agent Runner with every setting left exactly as the operator
1328
- * chose it — the one-flag install still just works.
1950
+ * or an allowlist BEFORE installing the chart), or that already
1951
+ * has a Kubernetes AI agent row that never ran kubectl here, is
1952
+ * bound to this agent Runner with every setting left exactly as
1953
+ * it is — the one-flag install still just works.
1329
1954
  * - that nobody ever configured takes the chart's intent:
1330
1955
  * investigation on, remediation "ask for approval" when the chart
1331
1956
  * also granted write RBAC (a mode already on the row is kept).
@@ -1366,7 +1991,14 @@ class KubernetesClusterAiAccessServiceClass {
1366
1991
  }
1367
1992
  } else if (hadRunnerBound || hasCommandHistory) {
1368
1993
  bindingState = "left_unbound_by_operator";
1369
- } else if (cluster.aiAccessConfiguredAt) {
1994
+ } else if (cluster.aiAccessConfiguredAt || aiAgent) {
1995
+ /*
1996
+ * Configured by an operator before the chart was installed, or the
1997
+ * cluster already has a Kubernetes AI agent (offline, or this
1998
+ * registration would have been refused) that has not run kubectl
1999
+ * here: bind, so AI works through this Runner, but leave every
2000
+ * switch — and the configured marker — exactly as they are.
2001
+ */
1370
2002
  bindingState = "bound_keeping_operator_settings";
1371
2003
 
1372
2004
  // Only the binding: not one of the operator's switches moves.
@@ -1402,7 +2034,9 @@ class KubernetesClusterAiAccessServiceClass {
1402
2034
 
1403
2035
  if (bindingState === "left_unbound_by_operator") {
1404
2036
  logger.info(
1405
- `KubernetesClusterAiAccess: cluster "${clusterIdentifier}" in project ${data.projectId.toString()} had a Runner bound before (or has run kubectl) but has none bound now — the binding was cleared, or its Runner was deleted; the in-cluster Runner registered without re-binding it.`,
2037
+ `KubernetesClusterAiAccess: cluster "${clusterIdentifier}" in project ${data.projectId.toString()} had a Runner bound before (or has run kubectl${
2038
+ aiAgent ? ", possibly through its Kubernetes AI agent" : ""
2039
+ }) but has none bound now; the in-cluster Runner registered without binding it.`,
1406
2040
  );
1407
2041
  }
1408
2042
 
@@ -1419,6 +2053,11 @@ class KubernetesClusterAiAccessServiceClass {
1419
2053
  agentRunnerExistedBefore,
1420
2054
  appliedRemediationMode,
1421
2055
  reKeyAdmission,
2056
+ hasAiAgent: Boolean(aiAgent),
2057
+ hadRunnerBound,
2058
+ aiAgentPodNamespace: aiAgent
2059
+ ? parseKubernetesAgentPosture(aiAgent.posture)?.podNamespace
2060
+ : undefined,
1422
2061
  });
1423
2062
 
1424
2063
  return {
@@ -1582,11 +2221,11 @@ class KubernetesClusterAiAccessServiceClass {
1582
2221
  */
1583
2222
  throw new KubernetesAgentRegistrationRefusedException({
1584
2223
  reason: "runner_holds_more_than_defaults",
1585
- message: `Under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(
2224
+ message: `Upgrade the Kubernetes agent chart: its Kubernetes AI agent replaces this in-cluster Runner. Or, under Project Settings → Runners, on Runner "${data.runner.name}": ${this.describeHoldingRemedies(
1586
2225
  holdings,
1587
- )} — or delete the Runner and, once the agent registers a fresh one on its next retry, select it on the cluster's AI page as its Runner. It is offline, but it holds more than an in-cluster Runner's defaults (${this.describeHoldings(
2226
+ )}. It is offline, but it holds more than an in-cluster Runner's defaults (${this.describeHoldings(
1588
2227
  holdings,
1589
- )}), so a registration for cluster "${data.clusterIdentifier}" that does not present its current key may not take it over, and its key was not changed. ${getDeletedAgentRunnerRebindNote()}`,
2228
+ )}), so a registration for cluster "${data.clusterIdentifier}" that does not present its current key may not take it over, and its key was not changed.`,
1590
2229
  });
1591
2230
  }
1592
2231
 
@@ -1605,9 +2244,10 @@ class KubernetesClusterAiAccessServiceClass {
1605
2244
  * kubectl access only (no runbooks, no code fixes, nothing assigned) bound
1606
2245
  * to its own cluster; anything else was entrusted to the row by an
1607
2246
  * operator. "Runs Runbooks" is read the way the claim path reads it: on
1608
- * unless explicitly false.
2247
+ * unless explicitly false. Also what decides whether an unused previous
2248
+ * in-cluster Runner may be retired (KubernetesAiAgentService).
1609
2249
  */
1610
- private async getRunnerHoldingsBeyondDefaults(data: {
2250
+ public async getRunnerHoldingsBeyondDefaults(data: {
1611
2251
  runner: Runner;
1612
2252
  clusterId: ObjectID;
1613
2253
  projectId: ObjectID;
@@ -1676,7 +2316,7 @@ class KubernetesClusterAiAccessServiceClass {
1676
2316
  if (otherBoundClusters > 0) {
1677
2317
  holdings.push({
1678
2318
  description: `the AI access Runner of ${otherBoundClusters} other cluster(s)`,
1679
- remedy: `select another Runner on the AI page of the ${otherBoundClusters} other cluster(s) bound to it`,
2319
+ remedy: `clear the Runner on the AI agent page of the ${otherBoundClusters} other cluster(s) bound to it`,
1680
2320
  });
1681
2321
  }
1682
2322
 
@@ -1750,6 +2390,12 @@ class KubernetesClusterAiAccessServiceClass {
1750
2390
  appliedRemediationMode?: KubernetesAiRemediationMode | undefined;
1751
2391
  // How an existing row's re-key was admitted; undefined for a new row.
1752
2392
  reKeyAdmission?: AgentRunnerReKeyAdmission | undefined;
2393
+ // The cluster has a Kubernetes AI agent row (offline: this one got in).
2394
+ hasAiAgent?: boolean | undefined;
2395
+ // A Runner was bound to the cluster before (aiAccessRunnerBoundAt).
2396
+ hadRunnerBound?: boolean | undefined;
2397
+ // The namespace the cluster's agent reported, when known.
2398
+ aiAgentPodNamespace?: string | undefined;
1753
2399
  }): Promise<void> {
1754
2400
  try {
1755
2401
  const runnerName: string = data.runner.name || "kubernetes-agent";
@@ -1768,15 +2414,36 @@ class KubernetesClusterAiAccessServiceClass {
1768
2414
  feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** registered from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI investigation is on; AI remediation is ${this.describeRemediationMode(
1769
2415
  data.appliedRemediationMode,
1770
2416
  )}.`;
2417
+ } else if (
2418
+ data.bindingState === "bound_keeping_operator_settings" &&
2419
+ data.hasAiAgent &&
2420
+ !data.cluster.aiAccessConfiguredAt
2421
+ ) {
2422
+ displayColor = Green500;
2423
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) while this cluster's Kubernetes AI agent is offline, and was bound so AI works through it: no Runner was bound here before and AI had not run kubectl here yet. No AI setting was changed. The Kubernetes AI agent takes over again as soon as it is back online.`;
1771
2424
  } else if (data.bindingState === "bound_keeping_operator_settings") {
1772
2425
  displayColor = Green500;
1773
- feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI access had already been configured on the cluster's AI page, so no AI setting was changed: AI investigation with kubectl is ${
2426
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} from the Kubernetes agent chart (${writes}) and was bound as this cluster's AI access Runner. AI access had already been configured on the cluster's AI agent page, so no AI setting was changed: AI investigation with kubectl is ${
1774
2427
  data.cluster.isAiInvestigationEnabled === true ? "on" : "off"
1775
2428
  } and AI remediation is ${this.describeRemediationMode(
1776
2429
  data.cluster.aiRemediationMode,
1777
2430
  )}, as an operator chose.`;
2431
+ } else if (
2432
+ data.bindingState === "left_unbound_by_operator" &&
2433
+ data.hasAiAgent &&
2434
+ !data.hadRunnerBound &&
2435
+ !data.cluster.aiAccessRunnerId
2436
+ ) {
2437
+ /*
2438
+ * No Runner was ever bound, so the history that kept this one
2439
+ * unbound is most likely the agent's own — not an operator's
2440
+ * doing. Say that, and where AI access comes back from.
2441
+ */
2442
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}) while this cluster's Kubernetes AI agent is offline, and was left unbound: AI has already run kubectl on this cluster (through its Kubernetes AI agent, or a Runner that is no longer bound), and a registering Runner is never bound over that history. No AI setting was changed. AI reaches this cluster again when its Kubernetes AI agent is back online: check the agent's pod with \`${getAiAgentLogsCommand(
2443
+ data.aiAgentPodNamespace,
2444
+ )}\`, or upgrade the Kubernetes agent chart again.`;
1778
2445
  } else if (data.bindingState === "left_unbound_by_operator") {
1779
- feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but no Runner is bound to this cluster although one was before (or AI already ran kubectl here) — the binding was cleared by an operator, or the Runner it was bound to was deleted — so it was left unbound and no AI switch was changed. Select **${runnerName}** on the cluster's AI page to use it.`;
2446
+ feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but no Runner is bound to this cluster although one was before (or AI already ran kubectl here) — the binding was cleared by an operator, or the Runner it was bound to was deleted — so it was left unbound and no AI switch was changed. Upgrade the Kubernetes agent chart to use the Kubernetes AI agent instead.`;
1780
2447
  } else if (data.bindingState === "bound_to_other_runner") {
1781
2448
  feedInfoInMarkdown = `🤖 The in-cluster Runner **${runnerName}** ${registered} (${writes}), but this cluster is bound to a different Runner in the dashboard, so it was not used.`;
1782
2449
  } else if (data.reKeyAdmission === "continuity") {