@oneuptime/common 14.0.9 → 14.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
- package/Models/DatabaseModels/CephCluster.ts +213 -0
- package/Models/DatabaseModels/DatabaseServer.ts +213 -0
- package/Models/DatabaseModels/DockerHost.ts +213 -0
- package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
- package/Models/DatabaseModels/GlobalConfig.ts +1 -1
- package/Models/DatabaseModels/Host.ts +213 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/PodmanHost.ts +213 -0
- package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
- package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
- package/Models/DatabaseModels/RunnerJob.ts +144 -0
- package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
- package/Server/API/AutoRemediationAPI.ts +392 -1
- package/Server/API/ResourceAiAccessAPI.ts +1669 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Semaphore.ts +22 -0
- package/Server/Middleware/TelemetryIngest.ts +15 -5
- package/Server/Services/AnalyticsDatabaseService.ts +45 -1
- package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
- package/Server/Services/CephClusterService.ts +109 -1
- package/Server/Services/DatabaseServerService.ts +116 -1
- package/Server/Services/DockerHostService.ts +109 -1
- package/Server/Services/DockerSwarmClusterService.ts +113 -1
- package/Server/Services/HostService.ts +126 -4
- package/Server/Services/MetricRecordingRuleService.ts +47 -0
- package/Server/Services/PodmanHostService.ts +109 -1
- package/Server/Services/ProxmoxClusterService.ts +110 -1
- package/Server/Services/ResourceAiAccessService.ts +1439 -0
- package/Server/Services/ResourceAiAgentJobService.ts +202 -0
- package/Server/Services/ResourceAiAgentService.ts +2432 -0
- package/Server/Services/RunnerJobService.ts +611 -4
- package/Server/Services/TelemetryUsageBillingService.ts +28 -0
- package/Server/Services/TraceRecordingRuleService.ts +33 -0
- package/Server/Services/VMwareVCenterService.ts +110 -1
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
- package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
- package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
- package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
- package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
- package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
- package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
- package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
- package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
- package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
- package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
- package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
- package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
- package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
- package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
- package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
- package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
- package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
- package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
- package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
- package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
- package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
- package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
- package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
- package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
- package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
- package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
- package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
- package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
- package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
- package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
- package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
- package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
- package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
- package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
- package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
- package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
- package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
- package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
- package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
- package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
- package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
- package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
- package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
- package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
- package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
- package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
- package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
- package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
- package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
- package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
- package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
- package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
- package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
- package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
- package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
- package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
- package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
- package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
- package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
- package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
- package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
- package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
- package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
- package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
- package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
- package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
- package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
- package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
- package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
- package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
- package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
- package/Types/AI/ResourceAiAccessApi.ts +140 -0
- package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
- package/Types/Monitor/RumAlertTemplates.ts +351 -4
- package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
- package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
- package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
- package/Types/Runbook/RunbookStepType.ts +29 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
- package/Utils/AI/InvestigationReport.ts +121 -15
- package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
- package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
- package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
- package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
- package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
- package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
- package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
- package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
- package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
- package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
- package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
- package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
- package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +219 -0
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
- package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
- package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
- package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +104 -0
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseServerService.js +99 -0
- package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +104 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +114 -2
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
- package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +104 -0
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
- package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
- package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
- package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/RunnerJobService.js +344 -5
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
- package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
- package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
- package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
- package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
- package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
- package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
- package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
- package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
- package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
- package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/Utils/AI/InvestigationReport.js +71 -11
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
- package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
- package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
- package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
- package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
- package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
- package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
- package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
- package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
- package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
import Alert from "../../Models/DatabaseModels/Alert";
|
|
2
|
+
import Incident from "../../Models/DatabaseModels/Incident";
|
|
3
|
+
import Monitor from "../../Models/DatabaseModels/Monitor";
|
|
4
|
+
import ResourceAiAgent from "../../Models/DatabaseModels/ResourceAiAgent";
|
|
5
|
+
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
|
+
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
7
|
+
import OneUptimeDate from "../../Types/Date";
|
|
8
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
|
+
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
|
10
|
+
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
11
|
+
import ObjectID from "../../Types/ObjectID";
|
|
12
|
+
import AiResourceType, {
|
|
13
|
+
AI_RESOURCE_TYPE_INFO,
|
|
14
|
+
ALL_AI_RESOURCE_TYPES,
|
|
15
|
+
AiResourceTypeInfo,
|
|
16
|
+
isAiResourceType,
|
|
17
|
+
} from "../../Types/ResourceAiAgent/AiResourceType";
|
|
18
|
+
import {
|
|
19
|
+
RESOURCE_AI_AGENT_IMAGE_REPOSITORY,
|
|
20
|
+
RESOURCE_AI_AGENT_RESOURCE_TYPE_ENV,
|
|
21
|
+
RESOURCE_AI_ALLOW_WRITES_ENV,
|
|
22
|
+
RESOURCE_AI_WRITE_TARGETS_ENV,
|
|
23
|
+
ResourceAiAccessGap,
|
|
24
|
+
ResourceAiAccessStatus,
|
|
25
|
+
ResourceAiAgentPosture,
|
|
26
|
+
ResourceAiAgentSummary,
|
|
27
|
+
ResourceAiRemediationMode,
|
|
28
|
+
parseResourceAiRemediationMode,
|
|
29
|
+
} from "../../Types/ResourceAiAgent/ResourceAiAccess";
|
|
30
|
+
import ModelPermission from "../Types/Database/Permissions/Index";
|
|
31
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
32
|
+
import Select from "../Types/Database/Select";
|
|
33
|
+
import MonitorResourceContextUtil from "../Utils/Monitor/MonitorResourceContext";
|
|
34
|
+
import { SeriesResolvedResourceIds } from "../Utils/Monitor/SeriesResourceLinker";
|
|
35
|
+
import logger from "../Utils/Logger";
|
|
36
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
37
|
+
import AlertService from "./AlertService";
|
|
38
|
+
import CephClusterService from "./CephClusterService";
|
|
39
|
+
import DatabaseServerService from "./DatabaseServerService";
|
|
40
|
+
import DatabaseService from "./DatabaseService";
|
|
41
|
+
import DockerHostService from "./DockerHostService";
|
|
42
|
+
import DockerSwarmClusterService from "./DockerSwarmClusterService";
|
|
43
|
+
import HostService from "./HostService";
|
|
44
|
+
import IncidentService from "./IncidentService";
|
|
45
|
+
import KubernetesClusterAiAccessService, {
|
|
46
|
+
AI_BALANCE_INSUFFICIENT_NEXT_STEP,
|
|
47
|
+
KubernetesClusterAiAccessProjectGates,
|
|
48
|
+
} from "./KubernetesClusterAiAccessService";
|
|
49
|
+
import MonitorService from "./MonitorService";
|
|
50
|
+
import PodmanHostService from "./PodmanHostService";
|
|
51
|
+
import ProxmoxClusterService from "./ProxmoxClusterService";
|
|
52
|
+
import ResourceAiAgentService from "./ResourceAiAgentService";
|
|
53
|
+
import VMwareVCenterService from "./VMwareVCenterService";
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* OneUptime AI access to an infrastructure resource — a Docker or Podman
|
|
57
|
+
* host, a Docker Swarm, Proxmox, VMware or Ceph cluster, a database server
|
|
58
|
+
* or a host — through that resource's AI agent (a ResourceAiAgent row).
|
|
59
|
+
*
|
|
60
|
+
* The resource-agnostic sibling of KubernetesClusterAiAccessService, and
|
|
61
|
+
* shaped like it: it answers, from CURRENT configuration, whether AI can
|
|
62
|
+
* run commands on a resource and what it may do there, and turns every
|
|
63
|
+
* reason it cannot into a row of a checklist with a next step. The same
|
|
64
|
+
* status feeds the AI runs (which only ever act on a resource this module
|
|
65
|
+
* calls ready), the investigation's prompt, and the resource's AI agent
|
|
66
|
+
* page.
|
|
67
|
+
*
|
|
68
|
+
* A resource is reached through exactly ONE target: its own resource AI
|
|
69
|
+
* agent. There are no Runners and no credentials here — the agent holds the
|
|
70
|
+
* resource's credentials itself and never receives one from OneUptime.
|
|
71
|
+
*
|
|
72
|
+
* The project-wide gates (AI on, a provider, a balance, auto-remediation)
|
|
73
|
+
* are the Kubernetes service's, read once per call and shared, so a project
|
|
74
|
+
* setting reads the same for a cluster and for every other resource.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
export type ResourceAiAccessProjectGates =
|
|
78
|
+
KubernetesClusterAiAccessProjectGates;
|
|
79
|
+
|
|
80
|
+
// One resource a subject (an incident or an alert) is linked to.
|
|
81
|
+
export interface ResourceAiAccessRef {
|
|
82
|
+
resourceType: AiResourceType;
|
|
83
|
+
resourceId: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* A resource row as the status computation reads it, whichever table it
|
|
88
|
+
* came from. Built by loadResource / loadResources, never by callers.
|
|
89
|
+
*/
|
|
90
|
+
export interface ResourceAiAccessRow {
|
|
91
|
+
resourceType: AiResourceType;
|
|
92
|
+
id: ObjectID;
|
|
93
|
+
projectId: ObjectID;
|
|
94
|
+
// The name the dashboard shows (the row's name, else its identifier).
|
|
95
|
+
name: string;
|
|
96
|
+
/*
|
|
97
|
+
* The identity the resource's collector reports and its agent registers
|
|
98
|
+
* with (hostIdentifier, the cluster/vCenter name, the database endpoint).
|
|
99
|
+
*/
|
|
100
|
+
identifier?: string | undefined;
|
|
101
|
+
isAiInvestigationEnabled: boolean;
|
|
102
|
+
aiRemediationMode: ResourceAiRemediationMode;
|
|
103
|
+
aiCommandAllowlist: Array<string>;
|
|
104
|
+
aiAccessConfiguredAt?: Date | undefined;
|
|
105
|
+
aiAccessLastVerifiedAt?: Date | undefined;
|
|
106
|
+
aiAccessLastError?: string | undefined;
|
|
107
|
+
isArchived: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* The resource and the agent a command for it would go to right now: the
|
|
112
|
+
* resource's ONLINE agent (ResourceAiAgentService.findOnlineAgentForResource),
|
|
113
|
+
* or null when it has none. resource is null when the resource does not
|
|
114
|
+
* exist in the project.
|
|
115
|
+
*/
|
|
116
|
+
export interface LoadedResourceAiAccessTarget {
|
|
117
|
+
resource: ResourceAiAccessRow | null;
|
|
118
|
+
agent: ResourceAiAgent | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The most resources one incident or alert offers an investigation.
|
|
122
|
+
export const MAX_RESOURCES_PER_SUBJECT: number = 10;
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* How many of a subject's linked resources are read per query while
|
|
126
|
+
* looking for its unarchived ones (getResourcesForSubject): one read covers
|
|
127
|
+
* any ordinary subject, and a subject linked to many archived resources is
|
|
128
|
+
* read on in batches until the cap is reached.
|
|
129
|
+
*/
|
|
130
|
+
const SUBJECT_RESOURCE_READ_BATCH_SIZE: number = MAX_RESOURCES_PER_SUBJECT * 5;
|
|
131
|
+
|
|
132
|
+
// The columns every resource model carries for AI access.
|
|
133
|
+
const AI_ACCESS_COLUMNS: Record<string, boolean> = {
|
|
134
|
+
_id: true,
|
|
135
|
+
projectId: true,
|
|
136
|
+
name: true,
|
|
137
|
+
isArchived: true,
|
|
138
|
+
isAiInvestigationEnabled: true,
|
|
139
|
+
aiRemediationMode: true,
|
|
140
|
+
aiCommandAllowlist: true,
|
|
141
|
+
aiAccessConfiguredAt: true,
|
|
142
|
+
aiAccessLastVerifiedAt: true,
|
|
143
|
+
aiAccessLastError: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type SubjectRelation =
|
|
147
|
+
| "dockerHosts"
|
|
148
|
+
| "podmanHosts"
|
|
149
|
+
| "dockerSwarmClusters"
|
|
150
|
+
| "proxmoxClusters"
|
|
151
|
+
| "vmwareVCenters"
|
|
152
|
+
| "cephClusters"
|
|
153
|
+
| "databaseServers"
|
|
154
|
+
| "hosts";
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
* How each resource type is read: its table's service, the extra columns
|
|
158
|
+
* its identity lives in, the Incident/Alert relation that links it to a
|
|
159
|
+
* subject, and the list the monitor-step fallback fills for it.
|
|
160
|
+
*/
|
|
161
|
+
interface ResourceAiAccessModelSpec {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
service: DatabaseService<any>;
|
|
164
|
+
identityColumns: Record<string, boolean>;
|
|
165
|
+
subjectRelation: SubjectRelation;
|
|
166
|
+
resolvedIdsKey: keyof SeriesResolvedResourceIds;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const MODEL_SPECS: Readonly<Record<AiResourceType, ResourceAiAccessModelSpec>> =
|
|
170
|
+
{
|
|
171
|
+
[AiResourceType.DockerHost]: {
|
|
172
|
+
service: DockerHostService,
|
|
173
|
+
identityColumns: { hostIdentifier: true },
|
|
174
|
+
subjectRelation: "dockerHosts",
|
|
175
|
+
resolvedIdsKey: "dockerHostIds",
|
|
176
|
+
},
|
|
177
|
+
[AiResourceType.PodmanHost]: {
|
|
178
|
+
service: PodmanHostService,
|
|
179
|
+
identityColumns: { hostIdentifier: true },
|
|
180
|
+
subjectRelation: "podmanHosts",
|
|
181
|
+
resolvedIdsKey: "podmanHostIds",
|
|
182
|
+
},
|
|
183
|
+
[AiResourceType.DockerSwarmCluster]: {
|
|
184
|
+
service: DockerSwarmClusterService,
|
|
185
|
+
identityColumns: {},
|
|
186
|
+
subjectRelation: "dockerSwarmClusters",
|
|
187
|
+
resolvedIdsKey: "dockerSwarmClusterIds",
|
|
188
|
+
},
|
|
189
|
+
[AiResourceType.ProxmoxCluster]: {
|
|
190
|
+
service: ProxmoxClusterService,
|
|
191
|
+
identityColumns: {},
|
|
192
|
+
subjectRelation: "proxmoxClusters",
|
|
193
|
+
resolvedIdsKey: "proxmoxClusterIds",
|
|
194
|
+
},
|
|
195
|
+
[AiResourceType.VMwareVCenter]: {
|
|
196
|
+
service: VMwareVCenterService,
|
|
197
|
+
identityColumns: {},
|
|
198
|
+
subjectRelation: "vmwareVCenters",
|
|
199
|
+
resolvedIdsKey: "vmwareVCenterIds",
|
|
200
|
+
},
|
|
201
|
+
[AiResourceType.CephCluster]: {
|
|
202
|
+
service: CephClusterService,
|
|
203
|
+
identityColumns: {},
|
|
204
|
+
subjectRelation: "cephClusters",
|
|
205
|
+
resolvedIdsKey: "cephClusterIds",
|
|
206
|
+
},
|
|
207
|
+
[AiResourceType.DatabaseServer]: {
|
|
208
|
+
service: DatabaseServerService,
|
|
209
|
+
identityColumns: {
|
|
210
|
+
dbSystem: true,
|
|
211
|
+
serverAddress: true,
|
|
212
|
+
serverPort: true,
|
|
213
|
+
},
|
|
214
|
+
subjectRelation: "databaseServers",
|
|
215
|
+
resolvedIdsKey: "databaseServerIds",
|
|
216
|
+
},
|
|
217
|
+
[AiResourceType.Host]: {
|
|
218
|
+
service: HostService,
|
|
219
|
+
identityColumns: { hostIdentifier: true },
|
|
220
|
+
subjectRelation: "hosts",
|
|
221
|
+
resolvedIdsKey: "hostIds",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* What an operator checks when the agent is online but cannot reach its
|
|
227
|
+
* resource: the one thing each agent needs besides the network.
|
|
228
|
+
*/
|
|
229
|
+
const REACH_HINTS: Readonly<Record<AiResourceType, string>> = {
|
|
230
|
+
[AiResourceType.DockerHost]:
|
|
231
|
+
"that the Docker socket (/var/run/docker.sock) is mounted into the agent's container",
|
|
232
|
+
[AiResourceType.PodmanHost]:
|
|
233
|
+
"that the Podman socket (/run/podman/podman.sock) is mounted into the agent's container",
|
|
234
|
+
[AiResourceType.DockerSwarmCluster]:
|
|
235
|
+
"that the agent runs on a swarm MANAGER node with the Docker socket mounted",
|
|
236
|
+
[AiResourceType.ProxmoxCluster]: "PVE_HOST and the agent's Proxmox API token",
|
|
237
|
+
[AiResourceType.VMwareVCenter]:
|
|
238
|
+
"the vCenter URL and the credentials in the agent's environment",
|
|
239
|
+
[AiResourceType.CephCluster]:
|
|
240
|
+
"the Ceph config and keyring mounted into the agent's container",
|
|
241
|
+
[AiResourceType.DatabaseServer]:
|
|
242
|
+
"the database address and the read-only credentials in the agent's environment",
|
|
243
|
+
[AiResourceType.Host]:
|
|
244
|
+
"that the agent runs privileged with the host's PID namespace (pid: host)",
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// "Docker host", but "host" and "database server" for the generic nouns.
|
|
248
|
+
export function describeResourceNoun(resourceType: AiResourceType): string {
|
|
249
|
+
if (resourceType === AiResourceType.Host) {
|
|
250
|
+
return "host";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (resourceType === AiResourceType.DatabaseServer) {
|
|
254
|
+
return "database server";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return isAiResourceType(resourceType)
|
|
258
|
+
? AI_RESOURCE_TYPE_INFO[resourceType].displayName
|
|
259
|
+
: "resource";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* Where every next step sends an operator: the resource's AI agent page
|
|
264
|
+
* (AI → AI agent in its side menu). Shown verbatim in the investigation's
|
|
265
|
+
* report and panel, so it always names the page.
|
|
266
|
+
*/
|
|
267
|
+
export function getResourceAiAgentPage(resourceType: AiResourceType): string {
|
|
268
|
+
return `the ${describeResourceNoun(resourceType)}'s AI agent page (AI → AI agent)`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/*
|
|
272
|
+
* How to install the resource's agent, as one sentence: the image, the
|
|
273
|
+
* type, and the identity to register with — spelled with this resource's
|
|
274
|
+
* own identity when it is known, so the agent lands on THIS row.
|
|
275
|
+
*/
|
|
276
|
+
export function getResourceAiAgentInstallNextStep(data: {
|
|
277
|
+
resourceType: AiResourceType;
|
|
278
|
+
resourceId?: string | undefined;
|
|
279
|
+
identifier?: string | undefined;
|
|
280
|
+
}): string {
|
|
281
|
+
const info: AiResourceTypeInfo = AI_RESOURCE_TYPE_INFO[data.resourceType];
|
|
282
|
+
const noun: string = describeResourceNoun(data.resourceType);
|
|
283
|
+
let identity: string;
|
|
284
|
+
|
|
285
|
+
if (data.resourceType === AiResourceType.DatabaseServer) {
|
|
286
|
+
identity = data.resourceId
|
|
287
|
+
? ` and DATABASE_SERVER_ID=${data.resourceId}`
|
|
288
|
+
: " and DATABASE_SERVER_ID set to this database server's id";
|
|
289
|
+
} else {
|
|
290
|
+
const variable: string = info.identityEnvVars[0] || "the identity";
|
|
291
|
+
identity = data.identifier
|
|
292
|
+
? ` and ${variable}=${data.identifier}`
|
|
293
|
+
: ` and ${variable} set to the name its collector reports`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return `Install the ${info.agentDisplayName} next to this ${noun}'s telemetry collector: run the ${RESOURCE_AI_AGENT_IMAGE_REPOSITORY} image with ${RESOURCE_AI_AGENT_RESOURCE_TYPE_ENV}=${info.agentAlias}${identity}. The complete snippet is on ${getResourceAiAgentPage(
|
|
297
|
+
data.resourceType,
|
|
298
|
+
)}.`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type DateLike = Date | string | null | undefined;
|
|
302
|
+
|
|
303
|
+
function toDate(value: DateLike): Date | null {
|
|
304
|
+
if (!value) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const date: Date = value instanceof Date ? value : new Date(value);
|
|
309
|
+
|
|
310
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toIsoString(value: DateLike): string | undefined {
|
|
314
|
+
const date: Date | null = toDate(value);
|
|
315
|
+
|
|
316
|
+
return date ? OneUptimeDate.toString(date) : undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* The allowlist column is jsonb; a client may save it as an array or as a
|
|
321
|
+
* JSON string. Anything unusable normalizes to empty — nothing risky
|
|
322
|
+
* auto-executes.
|
|
323
|
+
*/
|
|
324
|
+
export function normalizeResourceAllowlist(value: unknown): Array<string> {
|
|
325
|
+
let raw: unknown = value;
|
|
326
|
+
|
|
327
|
+
if (typeof raw === "string") {
|
|
328
|
+
try {
|
|
329
|
+
raw = JSON.parse(raw);
|
|
330
|
+
} catch {
|
|
331
|
+
raw = [value];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!Array.isArray(raw)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return raw
|
|
340
|
+
.filter((pattern: unknown): pattern is string => {
|
|
341
|
+
return typeof pattern === "string" && pattern.trim().length > 0;
|
|
342
|
+
})
|
|
343
|
+
.map((pattern: string): string => {
|
|
344
|
+
return pattern.trim();
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function refKey(resourceType: string, resourceId: string): string {
|
|
349
|
+
return `${resourceType}:${resourceId.toLowerCase()}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readString(value: unknown): string | undefined {
|
|
353
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export class ResourceAiAccessServiceClass {
|
|
357
|
+
/*
|
|
358
|
+
* ------------------------------------------------------------------
|
|
359
|
+
* Readiness
|
|
360
|
+
* ------------------------------------------------------------------
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
// The project-wide gates, read exactly as they are for clusters.
|
|
364
|
+
@CaptureSpan()
|
|
365
|
+
public async getProjectGates(
|
|
366
|
+
projectId: ObjectID,
|
|
367
|
+
): Promise<ResourceAiAccessProjectGates> {
|
|
368
|
+
return await KubernetesClusterAiAccessService.getProjectGates(projectId);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@CaptureSpan()
|
|
372
|
+
public async getStatusForResource(data: {
|
|
373
|
+
projectId: ObjectID;
|
|
374
|
+
resourceType: AiResourceType;
|
|
375
|
+
resourceId: ObjectID;
|
|
376
|
+
gates?: ResourceAiAccessProjectGates | undefined;
|
|
377
|
+
}): Promise<ResourceAiAccessStatus | null> {
|
|
378
|
+
const statuses: Array<ResourceAiAccessStatus> =
|
|
379
|
+
await this.getStatusesForResources({
|
|
380
|
+
projectId: data.projectId,
|
|
381
|
+
resources: [
|
|
382
|
+
{
|
|
383
|
+
resourceType: data.resourceType,
|
|
384
|
+
resourceId: data.resourceId.toString(),
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
gates: data.gates,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return statuses[0] || null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/*
|
|
394
|
+
* The status of several resources of one project, in the order asked
|
|
395
|
+
* for, with every resource's agent row read in ONE query. A resource
|
|
396
|
+
* that does not exist in the project (or, with excludeArchived, is
|
|
397
|
+
* archived) has no status. When the agent read fails every status reads
|
|
398
|
+
* as having no agent, which can only make it say less is reachable.
|
|
399
|
+
*/
|
|
400
|
+
@CaptureSpan()
|
|
401
|
+
public async getStatusesForResources(data: {
|
|
402
|
+
projectId: ObjectID;
|
|
403
|
+
resources: Array<ResourceAiAccessRef>;
|
|
404
|
+
gates?: ResourceAiAccessProjectGates | undefined;
|
|
405
|
+
excludeArchived?: boolean | undefined;
|
|
406
|
+
}): Promise<Array<ResourceAiAccessStatus>> {
|
|
407
|
+
const rows: Array<ResourceAiAccessRow> = await this.loadResources({
|
|
408
|
+
projectId: data.projectId,
|
|
409
|
+
resources: data.resources,
|
|
410
|
+
excludeArchived: data.excludeArchived,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (rows.length === 0) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const gates: ResourceAiAccessProjectGates =
|
|
418
|
+
data.gates || (await this.getProjectGates(data.projectId));
|
|
419
|
+
|
|
420
|
+
let agentRows: Map<string, ResourceAiAgent> = new Map<
|
|
421
|
+
string,
|
|
422
|
+
ResourceAiAgent
|
|
423
|
+
>();
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
agentRows = await this.findAgentRows({
|
|
427
|
+
projectId: data.projectId,
|
|
428
|
+
resources: rows,
|
|
429
|
+
});
|
|
430
|
+
} catch (error) {
|
|
431
|
+
logger.error(
|
|
432
|
+
`ResourceAiAccess: could not read the resource AI agents of project ${data.projectId.toString()}; every resource reads as having none: ${error}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return rows.map((row: ResourceAiAccessRow): ResourceAiAccessStatus => {
|
|
437
|
+
return this.buildStatus({
|
|
438
|
+
resource: row,
|
|
439
|
+
agentRow:
|
|
440
|
+
agentRows.get(refKey(row.resourceType, row.id.toString())) || null,
|
|
441
|
+
gates,
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/*
|
|
447
|
+
* The readiness computation proper, for one resource row, its agent row
|
|
448
|
+
* (online or not; null when none ever registered) and the project gates.
|
|
449
|
+
* Pure: every read was done by the caller.
|
|
450
|
+
*/
|
|
451
|
+
public buildStatus(data: {
|
|
452
|
+
resource: ResourceAiAccessRow;
|
|
453
|
+
agentRow: ResourceAiAgent | null;
|
|
454
|
+
gates: ResourceAiAccessProjectGates;
|
|
455
|
+
now?: Date | undefined;
|
|
456
|
+
}): ResourceAiAccessStatus {
|
|
457
|
+
const { resource, agentRow, gates } = data;
|
|
458
|
+
const type: AiResourceType = resource.resourceType;
|
|
459
|
+
const info: AiResourceTypeInfo = AI_RESOURCE_TYPE_INFO[type];
|
|
460
|
+
const noun: string = describeResourceNoun(type);
|
|
461
|
+
const page: string = getResourceAiAgentPage(type);
|
|
462
|
+
const gaps: Array<ResourceAiAccessGap> = [];
|
|
463
|
+
|
|
464
|
+
/*
|
|
465
|
+
* The agent as ResourceAiAgentService sees it: one online rule for the
|
|
466
|
+
* status and for the chokepoint (findOnlineAgentForResource).
|
|
467
|
+
*/
|
|
468
|
+
const agent: ResourceAiAgentSummary | null = agentRow
|
|
469
|
+
? ResourceAiAgentService.getAgentSummary(agentRow, data.now)
|
|
470
|
+
: null;
|
|
471
|
+
const posture: ResourceAiAgentPosture | null = agent?.posture || null;
|
|
472
|
+
const remediationMode: ResourceAiRemediationMode =
|
|
473
|
+
resource.aiRemediationMode;
|
|
474
|
+
|
|
475
|
+
if (!agent) {
|
|
476
|
+
gaps.push({
|
|
477
|
+
code: "ai_agent_not_connected",
|
|
478
|
+
title: `No ${info.agentDisplayName} is connected`,
|
|
479
|
+
nextStep: getResourceAiAgentInstallNextStep({
|
|
480
|
+
resourceType: type,
|
|
481
|
+
resourceId: resource.id.toString(),
|
|
482
|
+
identifier: resource.identifier,
|
|
483
|
+
}),
|
|
484
|
+
blocksInvestigation: true,
|
|
485
|
+
blocksRemediation: true,
|
|
486
|
+
});
|
|
487
|
+
} else if (!agent.isOnline) {
|
|
488
|
+
const lastAliveAt: Date | null = toDate(agentRow?.lastAliveAt);
|
|
489
|
+
const lastAliveText: string = lastAliveAt
|
|
490
|
+
? OneUptimeDate.getDateAsFormattedString(lastAliveAt)
|
|
491
|
+
: "an unknown time";
|
|
492
|
+
|
|
493
|
+
gaps.push({
|
|
494
|
+
code: "ai_agent_offline",
|
|
495
|
+
title: `The ${info.agentDisplayName} is offline`,
|
|
496
|
+
nextStep: `It last reported in at ${lastAliveText}${
|
|
497
|
+
agent.connectionStatus === "disconnected"
|
|
498
|
+
? " and then signed off (it was stopped, restarted or reset)"
|
|
499
|
+
: ""
|
|
500
|
+
}. Check that its container is running and can reach your OneUptime URL (its logs say why it cannot).`,
|
|
501
|
+
blocksInvestigation: true,
|
|
502
|
+
blocksRemediation: true,
|
|
503
|
+
});
|
|
504
|
+
} else if (!posture || posture.resourceType !== type) {
|
|
505
|
+
/*
|
|
506
|
+
* The agent always reports a posture for the type it serves; one
|
|
507
|
+
* that is missing, unreadable or for another type is not trusted
|
|
508
|
+
* with commands (the enqueue chokepoint refuses the same case).
|
|
509
|
+
*/
|
|
510
|
+
gaps.push({
|
|
511
|
+
code: "ai_agent_unreachable_resource",
|
|
512
|
+
title: `The ${info.agentDisplayName} has not reported that it can reach this ${noun}`,
|
|
513
|
+
nextStep: `Wait for its next report (within a minute), or reset the agent on ${page}.`,
|
|
514
|
+
blocksInvestigation: true,
|
|
515
|
+
blocksRemediation: true,
|
|
516
|
+
});
|
|
517
|
+
} else if (posture.reachable !== true) {
|
|
518
|
+
gaps.push({
|
|
519
|
+
code: "ai_agent_unreachable_resource",
|
|
520
|
+
title: `The ${info.agentDisplayName} cannot reach this ${noun}`,
|
|
521
|
+
nextStep: `Its last check failed${
|
|
522
|
+
posture.reachError ? `: ${posture.reachError}` : ""
|
|
523
|
+
}. Check ${REACH_HINTS[type]}, then use "Test connection" on ${page}.`,
|
|
524
|
+
blocksInvestigation: true,
|
|
525
|
+
blocksRemediation: true,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!resource.isAiInvestigationEnabled) {
|
|
530
|
+
gaps.push({
|
|
531
|
+
code: "investigation_disabled",
|
|
532
|
+
title: `AI investigation is turned off for this ${noun}`,
|
|
533
|
+
nextStep: `Turn on AI investigation on ${page}.`,
|
|
534
|
+
blocksInvestigation: true,
|
|
535
|
+
blocksRemediation: false,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (remediationMode === ResourceAiRemediationMode.Disabled) {
|
|
540
|
+
gaps.push({
|
|
541
|
+
code: "remediation_disabled",
|
|
542
|
+
title: `AI fixes are turned off for this ${noun}`,
|
|
543
|
+
nextStep: `Set "Fixes" to "Ask for approval", "Automatic" or "Bypass approval" on ${page}.`,
|
|
544
|
+
blocksInvestigation: false,
|
|
545
|
+
blocksRemediation: true,
|
|
546
|
+
});
|
|
547
|
+
} else if (agent && posture?.allowWrites !== true) {
|
|
548
|
+
gaps.push({
|
|
549
|
+
code: "remediation_write_access_missing",
|
|
550
|
+
title: `The ${info.agentDisplayName} is read-only`,
|
|
551
|
+
nextStep: `Set ${RESOURCE_AI_ALLOW_WRITES_ENV}=true on the ${info.agentDisplayName} and restart it (${RESOURCE_AI_WRITE_TARGETS_ENV} limits what it may change).`,
|
|
552
|
+
blocksInvestigation: false,
|
|
553
|
+
blocksRemediation: true,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!gates.isAiEnabled) {
|
|
558
|
+
gaps.push({
|
|
559
|
+
code: "ai_disabled_for_project",
|
|
560
|
+
title: "AI is disabled for this project",
|
|
561
|
+
nextStep: "Enable AI under Project Settings → AI Features.",
|
|
562
|
+
blocksInvestigation: true,
|
|
563
|
+
blocksRemediation: true,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!gates.hasLlmProvider) {
|
|
568
|
+
gaps.push({
|
|
569
|
+
code: "llm_provider_missing",
|
|
570
|
+
title: "No AI provider is configured",
|
|
571
|
+
nextStep:
|
|
572
|
+
"Add a provider under Project Settings → AI → LLM Providers (or use OneUptime AI credits).",
|
|
573
|
+
blocksInvestigation: true,
|
|
574
|
+
blocksRemediation: true,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (gates.aiBalanceBlocker) {
|
|
579
|
+
gaps.push({
|
|
580
|
+
code: "ai_balance_insufficient",
|
|
581
|
+
title: "The project is out of AI credits",
|
|
582
|
+
nextStep: AI_BALANCE_INSUFFICIENT_NEXT_STEP,
|
|
583
|
+
blocksInvestigation: true,
|
|
584
|
+
blocksRemediation: true,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!gates.isAutoRemediationEnabled) {
|
|
589
|
+
gaps.push({
|
|
590
|
+
code: "auto_remediation_disabled_for_project",
|
|
591
|
+
title: "Auto-remediation is disabled for this project",
|
|
592
|
+
nextStep:
|
|
593
|
+
"Enable auto-remediation under Project Settings → AI Features.",
|
|
594
|
+
blocksInvestigation: false,
|
|
595
|
+
blocksRemediation: true,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const blocksInvestigation: boolean = gaps.some(
|
|
600
|
+
(gap: ResourceAiAccessGap): boolean => {
|
|
601
|
+
return gap.blocksInvestigation;
|
|
602
|
+
},
|
|
603
|
+
);
|
|
604
|
+
const blocksRemediation: boolean = gaps.some(
|
|
605
|
+
(gap: ResourceAiAccessGap): boolean => {
|
|
606
|
+
return gap.blocksRemediation;
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
resourceType: type,
|
|
612
|
+
resourceId: resource.id.toString(),
|
|
613
|
+
resourceName: resource.name,
|
|
614
|
+
isAiInvestigationEnabled: resource.isAiInvestigationEnabled,
|
|
615
|
+
aiRemediationMode: remediationMode,
|
|
616
|
+
aiCommandAllowlist: resource.aiCommandAllowlist,
|
|
617
|
+
aiAccessConfiguredAt: toIsoString(resource.aiAccessConfiguredAt) || null,
|
|
618
|
+
aiAccessLastVerifiedAt:
|
|
619
|
+
toIsoString(resource.aiAccessLastVerifiedAt) || null,
|
|
620
|
+
aiAccessLastError: resource.aiAccessLastError || null,
|
|
621
|
+
agent,
|
|
622
|
+
gaps,
|
|
623
|
+
isInvestigationReady:
|
|
624
|
+
resource.isAiInvestigationEnabled && !blocksInvestigation,
|
|
625
|
+
isRemediationReady:
|
|
626
|
+
remediationMode !== ResourceAiRemediationMode.Disabled &&
|
|
627
|
+
!blocksRemediation,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/*
|
|
632
|
+
* ------------------------------------------------------------------
|
|
633
|
+
* Rows
|
|
634
|
+
* ------------------------------------------------------------------
|
|
635
|
+
*/
|
|
636
|
+
|
|
637
|
+
/*
|
|
638
|
+
* One resource of the project (as root: the caller scopes by project),
|
|
639
|
+
* or null when it is not the project's. Lookup failures propagate: the
|
|
640
|
+
* enqueue chokepoint must fail closed on a resource it could not read.
|
|
641
|
+
*/
|
|
642
|
+
@CaptureSpan()
|
|
643
|
+
public async loadResource(data: {
|
|
644
|
+
projectId: ObjectID;
|
|
645
|
+
resourceType: AiResourceType;
|
|
646
|
+
resourceId: ObjectID;
|
|
647
|
+
}): Promise<ResourceAiAccessRow | null> {
|
|
648
|
+
if (!isAiResourceType(data.resourceType)) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const rows: Array<ResourceAiAccessRow> = await this.loadResources({
|
|
653
|
+
projectId: data.projectId,
|
|
654
|
+
resources: [
|
|
655
|
+
{
|
|
656
|
+
resourceType: data.resourceType,
|
|
657
|
+
resourceId: data.resourceId.toString(),
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return rows[0] || null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/*
|
|
666
|
+
* Several resources of one project, one query per resource type, in the
|
|
667
|
+
* order asked for (duplicates and unknown types dropped).
|
|
668
|
+
*/
|
|
669
|
+
@CaptureSpan()
|
|
670
|
+
public async loadResources(data: {
|
|
671
|
+
projectId: ObjectID;
|
|
672
|
+
resources: Array<ResourceAiAccessRef>;
|
|
673
|
+
excludeArchived?: boolean | undefined;
|
|
674
|
+
}): Promise<Array<ResourceAiAccessRow>> {
|
|
675
|
+
const wanted: Array<ResourceAiAccessRef> = [];
|
|
676
|
+
const seen: Set<string> = new Set<string>();
|
|
677
|
+
|
|
678
|
+
for (const ref of data.resources) {
|
|
679
|
+
if (
|
|
680
|
+
!ref ||
|
|
681
|
+
!isAiResourceType(ref.resourceType) ||
|
|
682
|
+
typeof ref.resourceId !== "string" ||
|
|
683
|
+
!ObjectID.isValidUUID(ref.resourceId)
|
|
684
|
+
) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const key: string = refKey(ref.resourceType, ref.resourceId);
|
|
689
|
+
|
|
690
|
+
if (seen.has(key)) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
seen.add(key);
|
|
695
|
+
wanted.push(ref);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const byKey: Map<string, ResourceAiAccessRow> = new Map<
|
|
699
|
+
string,
|
|
700
|
+
ResourceAiAccessRow
|
|
701
|
+
>();
|
|
702
|
+
|
|
703
|
+
for (const type of ALL_AI_RESOURCE_TYPES) {
|
|
704
|
+
const ids: Array<string> = wanted
|
|
705
|
+
.filter((ref: ResourceAiAccessRef): boolean => {
|
|
706
|
+
return ref.resourceType === type;
|
|
707
|
+
})
|
|
708
|
+
.map((ref: ResourceAiAccessRef): string => {
|
|
709
|
+
return ref.resourceId;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
if (ids.length === 0) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const spec: ResourceAiAccessModelSpec = MODEL_SPECS[type];
|
|
717
|
+
|
|
718
|
+
const models: Array<Record<string, unknown>> = (await spec.service.findBy(
|
|
719
|
+
{
|
|
720
|
+
query: {
|
|
721
|
+
projectId: data.projectId,
|
|
722
|
+
_id: QueryHelper.any(
|
|
723
|
+
ids.map((id: string): ObjectID => {
|
|
724
|
+
return new ObjectID(id);
|
|
725
|
+
}),
|
|
726
|
+
),
|
|
727
|
+
...(data.excludeArchived ? { isArchived: false } : {}),
|
|
728
|
+
},
|
|
729
|
+
select: { ...AI_ACCESS_COLUMNS, ...spec.identityColumns },
|
|
730
|
+
limit: ids.length,
|
|
731
|
+
skip: 0,
|
|
732
|
+
props: { isRoot: true },
|
|
733
|
+
},
|
|
734
|
+
)) as unknown as Array<Record<string, unknown>>;
|
|
735
|
+
|
|
736
|
+
for (const model of models) {
|
|
737
|
+
const row: ResourceAiAccessRow | null = this.toRow(type, model, data);
|
|
738
|
+
|
|
739
|
+
if (row) {
|
|
740
|
+
byKey.set(refKey(type, row.id.toString()), row);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const rows: Array<ResourceAiAccessRow> = [];
|
|
746
|
+
|
|
747
|
+
for (const ref of wanted) {
|
|
748
|
+
const row: ResourceAiAccessRow | undefined = byKey.get(
|
|
749
|
+
refKey(ref.resourceType, ref.resourceId),
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
if (row) {
|
|
753
|
+
rows.push(row);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return rows;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A resource model read through its service, as the status reads it.
|
|
761
|
+
public toRow(
|
|
762
|
+
resourceType: AiResourceType,
|
|
763
|
+
model: Record<string, unknown>,
|
|
764
|
+
scope: { projectId: ObjectID; excludeArchived?: boolean | undefined },
|
|
765
|
+
): ResourceAiAccessRow | null {
|
|
766
|
+
const idValue: unknown = model["_id"] || model["id"];
|
|
767
|
+
const id: string | undefined =
|
|
768
|
+
idValue instanceof ObjectID
|
|
769
|
+
? idValue.toString()
|
|
770
|
+
: readString(idValue as string);
|
|
771
|
+
|
|
772
|
+
if (!id || !ObjectID.isValidUUID(id)) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const projectIdValue: unknown = model["projectId"];
|
|
777
|
+
const projectId: string | undefined =
|
|
778
|
+
projectIdValue instanceof ObjectID
|
|
779
|
+
? projectIdValue.toString()
|
|
780
|
+
: readString(projectIdValue as string);
|
|
781
|
+
|
|
782
|
+
// The query was project-scoped; a row that says otherwise is not used.
|
|
783
|
+
if (projectId && projectId !== scope.projectId.toString()) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const isArchived: boolean = model["isArchived"] === true;
|
|
788
|
+
|
|
789
|
+
if (scope.excludeArchived && isArchived) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const identifier: string | undefined = this.readIdentifier(
|
|
794
|
+
resourceType,
|
|
795
|
+
model,
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
resourceType,
|
|
800
|
+
id: new ObjectID(id),
|
|
801
|
+
projectId: scope.projectId,
|
|
802
|
+
name:
|
|
803
|
+
readString(model["name"]) ||
|
|
804
|
+
identifier ||
|
|
805
|
+
AI_RESOURCE_TYPE_INFO[resourceType].displayName,
|
|
806
|
+
identifier,
|
|
807
|
+
isAiInvestigationEnabled: model["isAiInvestigationEnabled"] === true,
|
|
808
|
+
aiRemediationMode: parseResourceAiRemediationMode(
|
|
809
|
+
model["aiRemediationMode"],
|
|
810
|
+
),
|
|
811
|
+
aiCommandAllowlist: normalizeResourceAllowlist(
|
|
812
|
+
model["aiCommandAllowlist"],
|
|
813
|
+
),
|
|
814
|
+
aiAccessConfiguredAt:
|
|
815
|
+
toDate(model["aiAccessConfiguredAt"] as DateLike) || undefined,
|
|
816
|
+
aiAccessLastVerifiedAt:
|
|
817
|
+
toDate(model["aiAccessLastVerifiedAt"] as DateLike) || undefined,
|
|
818
|
+
aiAccessLastError: readString(model["aiAccessLastError"]),
|
|
819
|
+
isArchived,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/*
|
|
824
|
+
* The identity the resource's collector reports: hostIdentifier for
|
|
825
|
+
* hosts, the name for clusters and vCenters, and system|host:port for a
|
|
826
|
+
* database server.
|
|
827
|
+
*/
|
|
828
|
+
private readIdentifier(
|
|
829
|
+
resourceType: AiResourceType,
|
|
830
|
+
model: Record<string, unknown>,
|
|
831
|
+
): string | undefined {
|
|
832
|
+
if (
|
|
833
|
+
resourceType === AiResourceType.DockerHost ||
|
|
834
|
+
resourceType === AiResourceType.PodmanHost ||
|
|
835
|
+
resourceType === AiResourceType.Host
|
|
836
|
+
) {
|
|
837
|
+
return readString(model["hostIdentifier"]);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (resourceType === AiResourceType.DatabaseServer) {
|
|
841
|
+
const address: string | undefined = readString(model["serverAddress"]);
|
|
842
|
+
|
|
843
|
+
if (!address) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const port: unknown = model["serverPort"];
|
|
848
|
+
const system: string | undefined = readString(model["dbSystem"]);
|
|
849
|
+
|
|
850
|
+
return `${system ? `${system}|` : ""}${address}${
|
|
851
|
+
typeof port === "number" && Number.isFinite(port) ? `:${port}` : ""
|
|
852
|
+
}`;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return readString(model["name"]);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/*
|
|
859
|
+
* The agent rows of these resources (online or not), keyed by
|
|
860
|
+
* type:resourceId — one query per resource type present
|
|
861
|
+
* (ResourceAiAgentService.findAgentsForResources). Never reads the key
|
|
862
|
+
* hash.
|
|
863
|
+
*/
|
|
864
|
+
@CaptureSpan()
|
|
865
|
+
public async findAgentRows(data: {
|
|
866
|
+
projectId: ObjectID;
|
|
867
|
+
resources: Array<Pick<ResourceAiAccessRow, "resourceType" | "id">>;
|
|
868
|
+
}): Promise<Map<string, ResourceAiAgent>> {
|
|
869
|
+
const byKey: Map<string, ResourceAiAgent> = new Map<
|
|
870
|
+
string,
|
|
871
|
+
ResourceAiAgent
|
|
872
|
+
>();
|
|
873
|
+
|
|
874
|
+
for (const type of ALL_AI_RESOURCE_TYPES) {
|
|
875
|
+
const ids: Array<ObjectID> = data.resources
|
|
876
|
+
.filter(
|
|
877
|
+
(
|
|
878
|
+
resource: Pick<ResourceAiAccessRow, "resourceType" | "id">,
|
|
879
|
+
): boolean => {
|
|
880
|
+
return resource.resourceType === type;
|
|
881
|
+
},
|
|
882
|
+
)
|
|
883
|
+
.map(
|
|
884
|
+
(
|
|
885
|
+
resource: Pick<ResourceAiAccessRow, "resourceType" | "id">,
|
|
886
|
+
): ObjectID => {
|
|
887
|
+
return resource.id;
|
|
888
|
+
},
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
if (ids.length === 0) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const rows: Map<string, ResourceAiAgent> =
|
|
896
|
+
await ResourceAiAgentService.findAgentsForResources({
|
|
897
|
+
projectId: data.projectId,
|
|
898
|
+
resourceType: type,
|
|
899
|
+
resourceIds: ids,
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
for (const [resourceId, row] of rows) {
|
|
903
|
+
byKey.set(refKey(type, resourceId), row);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return byKey;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/*
|
|
911
|
+
* ------------------------------------------------------------------
|
|
912
|
+
* The access target
|
|
913
|
+
* ------------------------------------------------------------------
|
|
914
|
+
*/
|
|
915
|
+
|
|
916
|
+
/*
|
|
917
|
+
* The resource and the agent a command for it goes to NOW: its online
|
|
918
|
+
* agent, or null. Lookup failures propagate: the enqueue chokepoint must
|
|
919
|
+
* fail closed on a target it could not check.
|
|
920
|
+
*/
|
|
921
|
+
@CaptureSpan()
|
|
922
|
+
public async loadAccessTarget(data: {
|
|
923
|
+
projectId: ObjectID;
|
|
924
|
+
resourceType: AiResourceType;
|
|
925
|
+
resourceId: ObjectID;
|
|
926
|
+
}): Promise<LoadedResourceAiAccessTarget> {
|
|
927
|
+
const resource: ResourceAiAccessRow | null = await this.loadResource(data);
|
|
928
|
+
|
|
929
|
+
if (!resource) {
|
|
930
|
+
return { resource: null, agent: null };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const agent: ResourceAiAgent | null =
|
|
934
|
+
await ResourceAiAgentService.findOnlineAgentForResource({
|
|
935
|
+
projectId: data.projectId,
|
|
936
|
+
resourceType: resource.resourceType,
|
|
937
|
+
resourceId: resource.id,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
return { resource, agent: agent || null };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/*
|
|
944
|
+
* ------------------------------------------------------------------
|
|
945
|
+
* Subjects
|
|
946
|
+
* ------------------------------------------------------------------
|
|
947
|
+
*/
|
|
948
|
+
|
|
949
|
+
/*
|
|
950
|
+
* The unarchived resources of the project an incident or alert is linked
|
|
951
|
+
* to, in the order ALL_AI_RESOURCE_TYPES lists the types, at most
|
|
952
|
+
* MAX_RESOURCES_PER_SUBJECT. Linked means the subject's resource
|
|
953
|
+
* relations (written when it was created, from its series labels and its
|
|
954
|
+
* monitors' configuration); when none of those is an unarchived resource
|
|
955
|
+
* of the project, the subject's monitors' step configuration is resolved
|
|
956
|
+
* again now (the resource may have appeared after the subject was
|
|
957
|
+
* created, or replaced an archived one), matching names
|
|
958
|
+
* case-insensitively like the linker does.
|
|
959
|
+
*
|
|
960
|
+
* Archived and missing resources are dropped BEFORE the cap: capping
|
|
961
|
+
* first would let ten archived links hide an active one behind them.
|
|
962
|
+
*/
|
|
963
|
+
@CaptureSpan()
|
|
964
|
+
public async getResourcesForSubject(data: {
|
|
965
|
+
projectId: ObjectID;
|
|
966
|
+
incidentId?: ObjectID | undefined;
|
|
967
|
+
alertId?: ObjectID | undefined;
|
|
968
|
+
}): Promise<Array<ResourceAiAccessRef>> {
|
|
969
|
+
const linked: Map<AiResourceType, Array<string>> = new Map<
|
|
970
|
+
AiResourceType,
|
|
971
|
+
Array<string>
|
|
972
|
+
>();
|
|
973
|
+
const monitorIds: Array<ObjectID> = [];
|
|
974
|
+
|
|
975
|
+
if (data.alertId || data.incidentId) {
|
|
976
|
+
const relations: Array<{
|
|
977
|
+
type: AiResourceType;
|
|
978
|
+
ids: Array<string>;
|
|
979
|
+
}> = await Promise.all(
|
|
980
|
+
ALL_AI_RESOURCE_TYPES.map(
|
|
981
|
+
async (
|
|
982
|
+
type: AiResourceType,
|
|
983
|
+
): Promise<{ type: AiResourceType; ids: Array<string> }> => {
|
|
984
|
+
return {
|
|
985
|
+
type,
|
|
986
|
+
ids: await this.readSubjectRelation({
|
|
987
|
+
projectId: data.projectId,
|
|
988
|
+
incidentId: data.incidentId,
|
|
989
|
+
alertId: data.alertId,
|
|
990
|
+
relation: MODEL_SPECS[type].subjectRelation,
|
|
991
|
+
}),
|
|
992
|
+
};
|
|
993
|
+
},
|
|
994
|
+
),
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
for (const relation of relations) {
|
|
998
|
+
linked.set(relation.type, relation.ids);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
monitorIds.push(
|
|
1002
|
+
...(await this.readSubjectMonitorIds({
|
|
1003
|
+
projectId: data.projectId,
|
|
1004
|
+
incidentId: data.incidentId,
|
|
1005
|
+
alertId: data.alertId,
|
|
1006
|
+
})),
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
let refs: Array<ResourceAiAccessRef> = await this.keepUnarchivedRefs({
|
|
1011
|
+
projectId: data.projectId,
|
|
1012
|
+
refs: this.flattenRefs(linked),
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (refs.length === 0 && monitorIds.length > 0) {
|
|
1016
|
+
refs = await this.keepUnarchivedRefs({
|
|
1017
|
+
projectId: data.projectId,
|
|
1018
|
+
refs: this.flattenRefs(
|
|
1019
|
+
await this.resolveMonitorResources({
|
|
1020
|
+
projectId: data.projectId,
|
|
1021
|
+
monitorIds,
|
|
1022
|
+
}),
|
|
1023
|
+
),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return refs;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/*
|
|
1031
|
+
* The first MAX_RESOURCES_PER_SUBJECT of these refs (in their order) that
|
|
1032
|
+
* are unarchived resources of the project, read in batches so a long
|
|
1033
|
+
* list of archived links costs a few reads, not one per link.
|
|
1034
|
+
*/
|
|
1035
|
+
private async keepUnarchivedRefs(data: {
|
|
1036
|
+
projectId: ObjectID;
|
|
1037
|
+
refs: Array<ResourceAiAccessRef>;
|
|
1038
|
+
}): Promise<Array<ResourceAiAccessRef>> {
|
|
1039
|
+
const kept: Array<ResourceAiAccessRef> = [];
|
|
1040
|
+
|
|
1041
|
+
for (
|
|
1042
|
+
let start: number = 0;
|
|
1043
|
+
start < data.refs.length && kept.length < MAX_RESOURCES_PER_SUBJECT;
|
|
1044
|
+
start += SUBJECT_RESOURCE_READ_BATCH_SIZE
|
|
1045
|
+
) {
|
|
1046
|
+
const rows: Array<ResourceAiAccessRow> = await this.loadResources({
|
|
1047
|
+
projectId: data.projectId,
|
|
1048
|
+
resources: data.refs.slice(
|
|
1049
|
+
start,
|
|
1050
|
+
start + SUBJECT_RESOURCE_READ_BATCH_SIZE,
|
|
1051
|
+
),
|
|
1052
|
+
excludeArchived: true,
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
for (const row of rows) {
|
|
1056
|
+
kept.push({
|
|
1057
|
+
resourceType: row.resourceType,
|
|
1058
|
+
resourceId: row.id.toString(),
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return kept.slice(0, MAX_RESOURCES_PER_SUBJECT);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
@CaptureSpan()
|
|
1067
|
+
public async getStatusesForSubject(data: {
|
|
1068
|
+
projectId: ObjectID;
|
|
1069
|
+
incidentId?: ObjectID | undefined;
|
|
1070
|
+
alertId?: ObjectID | undefined;
|
|
1071
|
+
}): Promise<Array<ResourceAiAccessStatus>> {
|
|
1072
|
+
const refs: Array<ResourceAiAccessRef> =
|
|
1073
|
+
await this.getResourcesForSubject(data);
|
|
1074
|
+
|
|
1075
|
+
if (refs.length === 0) {
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return await this.getStatusesForResources({
|
|
1080
|
+
projectId: data.projectId,
|
|
1081
|
+
resources: refs,
|
|
1082
|
+
excludeArchived: true,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Types in priority order, each type's ids in relation order, deduped.
|
|
1087
|
+
private flattenRefs(
|
|
1088
|
+
byType: Map<AiResourceType, Array<string>>,
|
|
1089
|
+
): Array<ResourceAiAccessRef> {
|
|
1090
|
+
const refs: Array<ResourceAiAccessRef> = [];
|
|
1091
|
+
const seen: Set<string> = new Set<string>();
|
|
1092
|
+
|
|
1093
|
+
for (const type of ALL_AI_RESOURCE_TYPES) {
|
|
1094
|
+
for (const id of byType.get(type) || []) {
|
|
1095
|
+
const key: string = refKey(type, id);
|
|
1096
|
+
|
|
1097
|
+
if (!ObjectID.isValidUUID(id) || seen.has(key)) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
seen.add(key);
|
|
1102
|
+
refs.push({ resourceType: type, resourceId: id });
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return refs;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/*
|
|
1110
|
+
* One of the subject's resource relations, read on its own: reading all
|
|
1111
|
+
* eight many-to-many relations in one query would multiply their rows.
|
|
1112
|
+
*/
|
|
1113
|
+
private async readSubjectRelation(data: {
|
|
1114
|
+
projectId: ObjectID;
|
|
1115
|
+
incidentId?: ObjectID | undefined;
|
|
1116
|
+
alertId?: ObjectID | undefined;
|
|
1117
|
+
relation: SubjectRelation;
|
|
1118
|
+
}): Promise<Array<string>> {
|
|
1119
|
+
const select: Record<string, unknown> = {
|
|
1120
|
+
_id: true,
|
|
1121
|
+
[data.relation]: { _id: true },
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
let linked: Array<{ id?: ObjectID | null | undefined }> = [];
|
|
1125
|
+
|
|
1126
|
+
if (data.alertId) {
|
|
1127
|
+
const alert: Alert | null = await AlertService.findOneBy({
|
|
1128
|
+
query: { _id: data.alertId.toString(), projectId: data.projectId },
|
|
1129
|
+
select: select as never,
|
|
1130
|
+
props: { isRoot: true },
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
linked = ((alert as unknown as Record<string, unknown> | null)?.[
|
|
1134
|
+
data.relation
|
|
1135
|
+
] || []) as Array<{ id?: ObjectID | null | undefined }>;
|
|
1136
|
+
} else if (data.incidentId) {
|
|
1137
|
+
const incident: Incident | null = await IncidentService.findOneBy({
|
|
1138
|
+
query: { _id: data.incidentId.toString(), projectId: data.projectId },
|
|
1139
|
+
select: select as never,
|
|
1140
|
+
props: { isRoot: true },
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
linked = ((incident as unknown as Record<string, unknown> | null)?.[
|
|
1144
|
+
data.relation
|
|
1145
|
+
] || []) as Array<{ id?: ObjectID | null | undefined }>;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const ids: Array<string> = [];
|
|
1149
|
+
|
|
1150
|
+
for (const resource of Array.isArray(linked) ? linked : []) {
|
|
1151
|
+
const id: string | undefined = resource?.id?.toString();
|
|
1152
|
+
|
|
1153
|
+
if (id) {
|
|
1154
|
+
ids.push(id);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return ids;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private async readSubjectMonitorIds(data: {
|
|
1162
|
+
projectId: ObjectID;
|
|
1163
|
+
incidentId?: ObjectID | undefined;
|
|
1164
|
+
alertId?: ObjectID | undefined;
|
|
1165
|
+
}): Promise<Array<ObjectID>> {
|
|
1166
|
+
if (data.alertId) {
|
|
1167
|
+
const alert: Alert | null = await AlertService.findOneBy({
|
|
1168
|
+
query: { _id: data.alertId.toString(), projectId: data.projectId },
|
|
1169
|
+
select: { _id: true, monitorId: true },
|
|
1170
|
+
props: { isRoot: true },
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
return alert?.monitorId ? [alert.monitorId] : [];
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (data.incidentId) {
|
|
1177
|
+
const incident: Incident | null = await IncidentService.findOneBy({
|
|
1178
|
+
query: { _id: data.incidentId.toString(), projectId: data.projectId },
|
|
1179
|
+
select: { _id: true, monitors: { _id: true } },
|
|
1180
|
+
props: { isRoot: true },
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
return (incident?.monitors || [])
|
|
1184
|
+
.map((monitor: Monitor): ObjectID | undefined => {
|
|
1185
|
+
return monitor.id || undefined;
|
|
1186
|
+
})
|
|
1187
|
+
.filter((id: ObjectID | undefined): id is ObjectID => {
|
|
1188
|
+
return Boolean(id);
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/*
|
|
1196
|
+
* What the subject's monitors' step configuration names now, resolved
|
|
1197
|
+
* the way the incident/alert creation path resolves it
|
|
1198
|
+
* (MonitorResourceContext: MonitorStepResourceIdentity, then
|
|
1199
|
+
* SeriesResourceLinker.resolveResourceRefs with case-insensitive
|
|
1200
|
+
* names). Best effort per monitor: that resolution never throws.
|
|
1201
|
+
*/
|
|
1202
|
+
private async resolveMonitorResources(data: {
|
|
1203
|
+
projectId: ObjectID;
|
|
1204
|
+
monitorIds: Array<ObjectID>;
|
|
1205
|
+
}): Promise<Map<AiResourceType, Array<string>>> {
|
|
1206
|
+
const byType: Map<AiResourceType, Array<string>> = new Map<
|
|
1207
|
+
AiResourceType,
|
|
1208
|
+
Array<string>
|
|
1209
|
+
>();
|
|
1210
|
+
|
|
1211
|
+
const monitors: Array<Monitor> = await MonitorService.findBy({
|
|
1212
|
+
query: {
|
|
1213
|
+
_id: QueryHelper.any(
|
|
1214
|
+
data.monitorIds.slice(0, MAX_RESOURCES_PER_SUBJECT),
|
|
1215
|
+
),
|
|
1216
|
+
projectId: data.projectId,
|
|
1217
|
+
},
|
|
1218
|
+
select: {
|
|
1219
|
+
_id: true,
|
|
1220
|
+
projectId: true,
|
|
1221
|
+
monitorType: true,
|
|
1222
|
+
monitorSteps: true,
|
|
1223
|
+
},
|
|
1224
|
+
limit: MAX_RESOURCES_PER_SUBJECT,
|
|
1225
|
+
skip: 0,
|
|
1226
|
+
props: { isRoot: true },
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
for (const monitor of monitors) {
|
|
1230
|
+
const resolved: SeriesResolvedResourceIds =
|
|
1231
|
+
await MonitorResourceContextUtil.resolveResourceContextForMonitor({
|
|
1232
|
+
monitor,
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
for (const type of ALL_AI_RESOURCE_TYPES) {
|
|
1236
|
+
const ids: Array<string> =
|
|
1237
|
+
(resolved[MODEL_SPECS[type].resolvedIdsKey] as
|
|
1238
|
+
| Array<string>
|
|
1239
|
+
| undefined) || [];
|
|
1240
|
+
|
|
1241
|
+
if (ids.length > 0) {
|
|
1242
|
+
byType.set(type, [...(byType.get(type) || []), ...ids]);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return byType;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/*
|
|
1251
|
+
* ------------------------------------------------------------------
|
|
1252
|
+
* Who may change a resource
|
|
1253
|
+
* ------------------------------------------------------------------
|
|
1254
|
+
*/
|
|
1255
|
+
|
|
1256
|
+
/*
|
|
1257
|
+
* Throws unless the caller may EDIT this resource: its update ACL, with
|
|
1258
|
+
* label-scoped allow and block rows decided from the row's own labels,
|
|
1259
|
+
* and the caller's Owned scope — exactly what a CRUD update of the row
|
|
1260
|
+
* would check. Approving an AI plan runs its commands on the resource as
|
|
1261
|
+
* root, so being allowed to start runbooks in the project is not enough
|
|
1262
|
+
* on its own: someone blocked from editing (or reading) a resource must
|
|
1263
|
+
* not be able to change it by approving a plan. Root and master admins
|
|
1264
|
+
* are not gated. A resource that does not exist in the project is
|
|
1265
|
+
* refused like one the caller may not edit.
|
|
1266
|
+
*/
|
|
1267
|
+
@CaptureSpan()
|
|
1268
|
+
public async assertCallerMayChangeResource(data: {
|
|
1269
|
+
props: DatabaseCommonInteractionProps;
|
|
1270
|
+
projectId: ObjectID;
|
|
1271
|
+
resourceType: AiResourceType;
|
|
1272
|
+
resourceId: ObjectID;
|
|
1273
|
+
}): Promise<void> {
|
|
1274
|
+
if (data.props.isRoot || data.props.isMasterAdmin) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (!isAiResourceType(data.resourceType)) {
|
|
1279
|
+
throw new BadDataException(
|
|
1280
|
+
`"${String(data.resourceType)}" is not a resource type.`,
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const noun: string = describeResourceNoun(data.resourceType);
|
|
1285
|
+
const refusal: string = `You do not have permission to edit this ${noun}, so you cannot approve an AI plan that changes it. Ask someone who may edit the ${noun} to approve it, or dismiss the suggestion.`;
|
|
1286
|
+
|
|
1287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1288
|
+
const service: DatabaseService<any> =
|
|
1289
|
+
MODEL_SPECS[data.resourceType].service;
|
|
1290
|
+
|
|
1291
|
+
const query: Record<string, unknown> = {
|
|
1292
|
+
_id: data.resourceId.toString(),
|
|
1293
|
+
projectId: data.projectId,
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
/*
|
|
1297
|
+
* The caller's permissions are read for the resource's own project —
|
|
1298
|
+
* the permission layer reads the rows of props.tenantId.
|
|
1299
|
+
*/
|
|
1300
|
+
const callerProps: DatabaseCommonInteractionProps = {
|
|
1301
|
+
...data.props,
|
|
1302
|
+
tenantId: data.projectId,
|
|
1303
|
+
isMultiTenantRequest: false,
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
/*
|
|
1307
|
+
* The row's labels, read as root: label-scoped allow and block rows are
|
|
1308
|
+
* decided per row, from them (DatabaseService.updateOneById's read).
|
|
1309
|
+
*/
|
|
1310
|
+
const select: Select<BaseModel> = { _id: true };
|
|
1311
|
+
const accessControlColumn: string | null = service
|
|
1312
|
+
.getModel()
|
|
1313
|
+
.getAccessControlColumn();
|
|
1314
|
+
|
|
1315
|
+
if (accessControlColumn) {
|
|
1316
|
+
(select as Record<string, unknown>)[accessControlColumn] = {
|
|
1317
|
+
_id: true,
|
|
1318
|
+
name: true,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const row: BaseModel | null = (await service.findOneBy({
|
|
1323
|
+
query,
|
|
1324
|
+
select,
|
|
1325
|
+
props: { isRoot: true },
|
|
1326
|
+
})) as BaseModel | null;
|
|
1327
|
+
|
|
1328
|
+
if (!row) {
|
|
1329
|
+
throw new NotAuthorizedException(refusal);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
// Readable to the caller at all (a label-scoped read block refuses).
|
|
1334
|
+
const readable: BaseModel | null = (await service.findOneBy({
|
|
1335
|
+
query,
|
|
1336
|
+
select: { _id: true },
|
|
1337
|
+
props: callerProps,
|
|
1338
|
+
})) as BaseModel | null;
|
|
1339
|
+
|
|
1340
|
+
if (!readable) {
|
|
1341
|
+
throw new NotAuthorizedException(refusal);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
await ModelPermission.checkUpdatePermissionByModel({
|
|
1345
|
+
modelType: service.modelType,
|
|
1346
|
+
fetchModelWithAccessControlIds: async (): Promise<BaseModel> => {
|
|
1347
|
+
return row;
|
|
1348
|
+
},
|
|
1349
|
+
props: callerProps,
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
/*
|
|
1353
|
+
* The caller's whole update scope as a query (labels, Owned scope):
|
|
1354
|
+
* the row must still be in it.
|
|
1355
|
+
*/
|
|
1356
|
+
const permittedQuery: Record<string, unknown> =
|
|
1357
|
+
(await ModelPermission.checkUpdateQueryPermissions(
|
|
1358
|
+
service.modelType,
|
|
1359
|
+
query,
|
|
1360
|
+
{},
|
|
1361
|
+
callerProps,
|
|
1362
|
+
)) as Record<string, unknown>;
|
|
1363
|
+
|
|
1364
|
+
const permitted: BaseModel | null = (await service.findOneBy({
|
|
1365
|
+
query: permittedQuery,
|
|
1366
|
+
select: { _id: true },
|
|
1367
|
+
props: { isRoot: true },
|
|
1368
|
+
})) as BaseModel | null;
|
|
1369
|
+
|
|
1370
|
+
if (!permitted) {
|
|
1371
|
+
throw new NotAuthorizedException(refusal);
|
|
1372
|
+
}
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (
|
|
1375
|
+
error instanceof NotAuthorizedException ||
|
|
1376
|
+
error instanceof NotAuthenticatedException
|
|
1377
|
+
) {
|
|
1378
|
+
throw new NotAuthorizedException(refusal);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/*
|
|
1386
|
+
* ------------------------------------------------------------------
|
|
1387
|
+
* Bookkeeping
|
|
1388
|
+
* ------------------------------------------------------------------
|
|
1389
|
+
*/
|
|
1390
|
+
|
|
1391
|
+
/*
|
|
1392
|
+
* Best effort: the resource's AI agent page shows "last verified" / "last
|
|
1393
|
+
* error" from this. Written as root without the model's hooks — it is the
|
|
1394
|
+
* server's own record of what a command did, not an operator's change to
|
|
1395
|
+
* the resource's AI settings (the settings hooks stamp and police those).
|
|
1396
|
+
*/
|
|
1397
|
+
@CaptureSpan()
|
|
1398
|
+
public async recordCommandOutcome(data: {
|
|
1399
|
+
resourceType: AiResourceType;
|
|
1400
|
+
resourceId: ObjectID;
|
|
1401
|
+
succeeded: boolean;
|
|
1402
|
+
errorMessage?: string | undefined;
|
|
1403
|
+
}): Promise<void> {
|
|
1404
|
+
try {
|
|
1405
|
+
if (!isAiResourceType(data.resourceType)) {
|
|
1406
|
+
throw new BadDataException(
|
|
1407
|
+
`"${String(data.resourceType)}" is not a resource type.`,
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
await MODEL_SPECS[data.resourceType].service.updateOneById({
|
|
1412
|
+
id: data.resourceId,
|
|
1413
|
+
data: (data.succeeded
|
|
1414
|
+
? {
|
|
1415
|
+
aiAccessLastVerifiedAt: OneUptimeDate.getCurrentDate(),
|
|
1416
|
+
aiAccessLastError: null,
|
|
1417
|
+
}
|
|
1418
|
+
: {
|
|
1419
|
+
aiAccessLastError: (data.errorMessage || "Command failed.").slice(
|
|
1420
|
+
0,
|
|
1421
|
+
2000,
|
|
1422
|
+
),
|
|
1423
|
+
}) as never,
|
|
1424
|
+
props: { isRoot: true, ignoreHooks: true },
|
|
1425
|
+
});
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
logger.error(
|
|
1428
|
+
`ResourceAiAccess: could not record a command outcome for ${String(
|
|
1429
|
+
data.resourceType,
|
|
1430
|
+
)} ${data.resourceId?.toString()}: ${error}`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const ResourceAiAccessService: ResourceAiAccessServiceClass =
|
|
1437
|
+
new ResourceAiAccessServiceClass();
|
|
1438
|
+
|
|
1439
|
+
export default ResourceAiAccessService;
|