@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,1220 @@
|
|
|
1
|
+
import ResourceAiAccessSettings, {
|
|
2
|
+
NEVER_CONFIGURED_RESOURCE_AI_ACCESS,
|
|
3
|
+
RESOURCE_AI_ACCESS_SERVER_ONLY_KEYS,
|
|
4
|
+
RESOURCE_AI_ACCESS_SETTING_KEYS,
|
|
5
|
+
RESOURCE_REMEDIATION_MODES_BY_AUTONOMY,
|
|
6
|
+
ResourceAiAccessFeedItem,
|
|
7
|
+
ResourceAiAccessSettingsSnapshot,
|
|
8
|
+
ResourceAiAccessWriteCarryForward,
|
|
9
|
+
} from "../../../../../Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings";
|
|
10
|
+
import DatabaseService from "../../../../../Server/Services/DatabaseService";
|
|
11
|
+
import UserService from "../../../../../Server/Services/UserService";
|
|
12
|
+
import { OnUpdate } from "../../../../../Server/Types/Database/Hooks";
|
|
13
|
+
import UpdateBy from "../../../../../Server/Types/Database/UpdateBy";
|
|
14
|
+
import logger from "../../../../../Server/Utils/Logger";
|
|
15
|
+
import BaseModel from "../../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
16
|
+
import {
|
|
17
|
+
RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS,
|
|
18
|
+
RESOURCE_AI_ALLOWLIST_EXAMPLES,
|
|
19
|
+
RESOURCE_AI_REMEDIATION_MODE_LABELS,
|
|
20
|
+
getResourceAgentName,
|
|
21
|
+
getResourceAiAccessAdminRefusal,
|
|
22
|
+
getResourceAiAgentResetRefusal,
|
|
23
|
+
getResourceSentenceName,
|
|
24
|
+
} from "../../../../../Types/AI/ResourceAiAccessPermissions";
|
|
25
|
+
import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
26
|
+
import { Gray500, Yellow500 } from "../../../../../Types/BrandColors";
|
|
27
|
+
import BadDataException from "../../../../../Types/Exception/BadDataException";
|
|
28
|
+
import NotAuthorizedException from "../../../../../Types/Exception/NotAuthorizedException";
|
|
29
|
+
import { JSONObject } from "../../../../../Types/JSON";
|
|
30
|
+
import { KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS } from "../../../../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
|
|
31
|
+
import { KubernetesAiRemediationMode } from "../../../../../Types/Kubernetes/KubernetesClusterAiAccess";
|
|
32
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
33
|
+
import Permission, {
|
|
34
|
+
UserPermission,
|
|
35
|
+
UserTenantAccessPermission,
|
|
36
|
+
} from "../../../../../Types/Permission";
|
|
37
|
+
import AiResourceType, {
|
|
38
|
+
ALL_AI_RESOURCE_TYPES,
|
|
39
|
+
} from "../../../../../Types/ResourceAiAgent/AiResourceType";
|
|
40
|
+
import {
|
|
41
|
+
ResourceAiRemediationMode,
|
|
42
|
+
ResourceCommandTier,
|
|
43
|
+
} from "../../../../../Types/ResourceAiAgent/ResourceAiAccess";
|
|
44
|
+
import ResourceCommandPolicy, {
|
|
45
|
+
RESOURCE_ALLOWLIST_MAX_PATTERNS,
|
|
46
|
+
} from "../../../../../Utils/AiRemediation/Resource/ResourceCommandPolicy";
|
|
47
|
+
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* The rules every resource AI agent's resource applies to an operator's
|
|
51
|
+
* write of its AI access settings (ResourceAiAccessSettings), pure and
|
|
52
|
+
* through a stand-in service: validation, who may make AI do more, the
|
|
53
|
+
* server-only columns, the configured marker and the feed item. The
|
|
54
|
+
* services' own wiring is pinned in
|
|
55
|
+
* Tests/Server/Services/ResourceAiSettingsPermission.test.ts.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
59
|
+
"22222222-2222-4222-8222-222222222222",
|
|
60
|
+
);
|
|
61
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
62
|
+
"99999999-9999-4999-8999-999999999999",
|
|
63
|
+
);
|
|
64
|
+
const RESOURCE_ID: ObjectID = new ObjectID(
|
|
65
|
+
"33333333-3333-4333-8333-333333333333",
|
|
66
|
+
);
|
|
67
|
+
const OTHER_RESOURCE_ID: ObjectID = new ObjectID(
|
|
68
|
+
"66666666-6666-4666-8666-666666666666",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
function permissionRow(
|
|
72
|
+
permission: Permission,
|
|
73
|
+
isBlockPermission: boolean = false,
|
|
74
|
+
): UserPermission {
|
|
75
|
+
return {
|
|
76
|
+
_type: "UserPermission",
|
|
77
|
+
permission,
|
|
78
|
+
labelIds: [],
|
|
79
|
+
isBlockPermission,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function userProps(
|
|
84
|
+
rows: Array<UserPermission>,
|
|
85
|
+
options: {
|
|
86
|
+
tenantId?: ObjectID | null;
|
|
87
|
+
permissionProjectId?: ObjectID;
|
|
88
|
+
} = {},
|
|
89
|
+
): DatabaseCommonInteractionProps {
|
|
90
|
+
const tenantId: ObjectID | null =
|
|
91
|
+
options.tenantId === undefined ? PROJECT_ID : options.tenantId;
|
|
92
|
+
const permissionProjectId: ObjectID =
|
|
93
|
+
options.permissionProjectId || tenantId || PROJECT_ID;
|
|
94
|
+
|
|
95
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
96
|
+
_type: "UserTenantAccessPermission",
|
|
97
|
+
projectId: permissionProjectId,
|
|
98
|
+
permissions: rows,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
userId: ObjectID.generate(),
|
|
103
|
+
...(tenantId ? { tenantId } : {}),
|
|
104
|
+
userTenantAccessPermission: {
|
|
105
|
+
[permissionProjectId.toString()]: tenantPermission,
|
|
106
|
+
},
|
|
107
|
+
} as DatabaseCommonInteractionProps;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function propsWith(
|
|
111
|
+
...permissions: Array<Permission>
|
|
112
|
+
): DatabaseCommonInteractionProps {
|
|
113
|
+
return userProps(
|
|
114
|
+
permissions.map((permission: Permission) => {
|
|
115
|
+
return permissionRow(permission);
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function snapshot(
|
|
121
|
+
overrides: Partial<ResourceAiAccessSettingsSnapshot> = {},
|
|
122
|
+
): ResourceAiAccessSettingsSnapshot {
|
|
123
|
+
return {
|
|
124
|
+
projectId: PROJECT_ID,
|
|
125
|
+
isAiInvestigationEnabled: false,
|
|
126
|
+
aiRemediationMode: ResourceAiRemediationMode.Disabled,
|
|
127
|
+
aiCommandAllowlist: [],
|
|
128
|
+
...overrides,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* A stand-in for a resource's DatabaseService: the settings read, the
|
|
134
|
+
* marker write and the project lookup are all the helper asks of it.
|
|
135
|
+
*/
|
|
136
|
+
interface FakeService {
|
|
137
|
+
findBy: jest.Mock;
|
|
138
|
+
findOneById: jest.Mock;
|
|
139
|
+
updateBy: jest.Mock;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fakeService(rows: Array<Record<string, unknown>> = []): FakeService {
|
|
143
|
+
return {
|
|
144
|
+
findBy: jest.fn().mockResolvedValue(rows),
|
|
145
|
+
findOneById: jest.fn().mockResolvedValue({ projectId: PROJECT_ID }),
|
|
146
|
+
updateBy: jest.fn().mockResolvedValue(1),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function asService(service: FakeService): DatabaseService<BaseModel> {
|
|
151
|
+
return service as unknown as DatabaseService<BaseModel>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function row(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
155
|
+
return {
|
|
156
|
+
id: RESOURCE_ID,
|
|
157
|
+
_id: RESOURCE_ID.toString(),
|
|
158
|
+
projectId: PROJECT_ID,
|
|
159
|
+
isAiInvestigationEnabled: false,
|
|
160
|
+
aiRemediationMode: ResourceAiRemediationMode.Disabled,
|
|
161
|
+
aiCommandAllowlist: null,
|
|
162
|
+
...overrides,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function updateBy(
|
|
167
|
+
data: Record<string, unknown>,
|
|
168
|
+
props: DatabaseCommonInteractionProps,
|
|
169
|
+
): UpdateBy<BaseModel> {
|
|
170
|
+
return {
|
|
171
|
+
query: { _id: RESOURCE_ID.toString() },
|
|
172
|
+
data,
|
|
173
|
+
limit: 1,
|
|
174
|
+
skip: 0,
|
|
175
|
+
props,
|
|
176
|
+
} as unknown as UpdateBy<BaseModel>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Roles that may edit a resource but may not author a FullAuto rule.
|
|
180
|
+
const EDITOR_ROLES: Array<Permission> = [
|
|
181
|
+
Permission.SettingsMember,
|
|
182
|
+
Permission.SettingsAdmin,
|
|
183
|
+
Permission.ProjectMember,
|
|
184
|
+
Permission.EditDockerHost,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
describe("ResourceAiAccessPermissions", () => {
|
|
188
|
+
it("takes exactly the Kubernetes cluster's admin set, so the two can never drift apart", () => {
|
|
189
|
+
expect([...RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS].sort()).toEqual(
|
|
190
|
+
[...KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS].sort(),
|
|
191
|
+
);
|
|
192
|
+
expect(RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS).toEqual([
|
|
193
|
+
Permission.ProjectOwner,
|
|
194
|
+
Permission.ProjectAdmin,
|
|
195
|
+
Permission.EditAutoRemediationRule,
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("names the permissions and what stays open, with the resource named in the sentence", () => {
|
|
200
|
+
expect(getResourceAiAccessAdminRefusal(AiResourceType.DockerHost)).toBe(
|
|
201
|
+
"You need one of these permissions to let OneUptime AI do more on a Docker host (turn AI fixes on or give them more autonomy, or add command allowlist patterns): Project Owner, Project Admin, Edit Auto Remediation Rule. Anyone who may edit the Docker host can still turn AI fixes off or down to Ask for approval, and remove allowlist patterns.",
|
|
202
|
+
);
|
|
203
|
+
expect(
|
|
204
|
+
getResourceAiAccessAdminRefusal(AiResourceType.DatabaseServer),
|
|
205
|
+
).toContain("do more on a database server");
|
|
206
|
+
expect(getResourceAiAccessAdminRefusal(AiResourceType.Host)).toContain(
|
|
207
|
+
"Anyone who may edit the host can",
|
|
208
|
+
);
|
|
209
|
+
expect(getResourceAiAgentResetRefusal(AiResourceType.CephCluster)).toBe(
|
|
210
|
+
"You need one of these permissions to reset this Ceph cluster's Ceph AI agent: Project Owner, Project Admin, Edit Auto Remediation Rule.",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it.each(ALL_AI_RESOURCE_TYPES)(
|
|
215
|
+
"has a sentence name, an agent name and a valid allowlist example for %s",
|
|
216
|
+
(resourceType: AiResourceType) => {
|
|
217
|
+
expect(getResourceSentenceName(resourceType)).toMatch(/^[A-Za-z]/);
|
|
218
|
+
expect(getResourceAgentName(resourceType)).toMatch(/AI agent$/);
|
|
219
|
+
|
|
220
|
+
const example: string = RESOURCE_AI_ALLOWLIST_EXAMPLES[resourceType];
|
|
221
|
+
expect(
|
|
222
|
+
ResourceCommandPolicy.describeAllowlistPatternProblem({
|
|
223
|
+
resourceType,
|
|
224
|
+
pattern: example,
|
|
225
|
+
}),
|
|
226
|
+
).toBeNull();
|
|
227
|
+
// An example worth allowlisting: a change that would otherwise ask.
|
|
228
|
+
expect(
|
|
229
|
+
ResourceCommandPolicy.evaluateCommand({
|
|
230
|
+
resourceType,
|
|
231
|
+
command: example,
|
|
232
|
+
}).tier,
|
|
233
|
+
).toBe(ResourceCommandTier.RiskyWrite);
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
it("labels every mode the way a Kubernetes cluster's AI page does", () => {
|
|
238
|
+
expect(RESOURCE_AI_REMEDIATION_MODE_LABELS).toEqual({
|
|
239
|
+
[ResourceAiRemediationMode.Disabled]: "Off",
|
|
240
|
+
[ResourceAiRemediationMode.RequireApproval]: "Ask for approval",
|
|
241
|
+
[ResourceAiRemediationMode.Automatic]: "Automatic",
|
|
242
|
+
[ResourceAiRemediationMode.BypassApproval]: "Bypass approval",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("ResourceAiAccessSettings constants", () => {
|
|
248
|
+
it("covers the three operator settings and the three server-only columns", () => {
|
|
249
|
+
expect([...RESOURCE_AI_ACCESS_SETTING_KEYS]).toEqual([
|
|
250
|
+
"isAiInvestigationEnabled",
|
|
251
|
+
"aiRemediationMode",
|
|
252
|
+
"aiCommandAllowlist",
|
|
253
|
+
]);
|
|
254
|
+
expect([...RESOURCE_AI_ACCESS_SERVER_ONLY_KEYS]).toEqual([
|
|
255
|
+
"aiAccessConfiguredAt",
|
|
256
|
+
"aiAccessLastVerifiedAt",
|
|
257
|
+
"aiAccessLastError",
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("orders the modes by autonomy with the Kubernetes values", () => {
|
|
262
|
+
expect([...RESOURCE_REMEDIATION_MODES_BY_AUTONOMY]).toEqual([
|
|
263
|
+
KubernetesAiRemediationMode.Disabled,
|
|
264
|
+
KubernetesAiRemediationMode.RequireApproval,
|
|
265
|
+
KubernetesAiRemediationMode.Automatic,
|
|
266
|
+
KubernetesAiRemediationMode.BypassApproval,
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("starts a never-configured resource from the column defaults: investigation off, fixes Off", () => {
|
|
271
|
+
expect(NEVER_CONFIGURED_RESOURCE_AI_ACCESS).toEqual({
|
|
272
|
+
isAiInvestigationEnabled: false,
|
|
273
|
+
aiRemediationMode: ResourceAiRemediationMode.Disabled,
|
|
274
|
+
aiCommandAllowlist: [],
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("ResourceAiAccessSettings.validateSettings", () => {
|
|
280
|
+
function validate(
|
|
281
|
+
data: Record<string, unknown>,
|
|
282
|
+
resourceType: AiResourceType = AiResourceType.DockerHost,
|
|
283
|
+
): JSONObject {
|
|
284
|
+
const copy: JSONObject = { ...data } as JSONObject;
|
|
285
|
+
ResourceAiAccessSettings.validateSettings({ resourceType, data: copy });
|
|
286
|
+
return copy;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
it.each(Object.values(ResourceAiRemediationMode))(
|
|
290
|
+
"accepts the mode %s",
|
|
291
|
+
(mode: string) => {
|
|
292
|
+
expect(() => {
|
|
293
|
+
return validate({ aiRemediationMode: mode });
|
|
294
|
+
}).not.toThrow();
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
it.each([["automatic"], ["Bypass"], [""], [null], [1], [true]])(
|
|
299
|
+
"refuses the unknown mode %p, naming the four it may be",
|
|
300
|
+
(mode: unknown) => {
|
|
301
|
+
expect(() => {
|
|
302
|
+
return validate({ aiRemediationMode: mode });
|
|
303
|
+
}).toThrow(
|
|
304
|
+
"AI remediation mode must be one of Disabled, RequireApproval, Automatic, BypassApproval",
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
it("leaves a write without AI settings untouched", () => {
|
|
310
|
+
expect(validate({ name: "web-1" })).toEqual({ name: "web-1" });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("stores an allowlist trimmed, and a JSON-encoded one as the array", () => {
|
|
314
|
+
expect(
|
|
315
|
+
validate({ aiCommandAllowlist: [" docker stop web "] })[
|
|
316
|
+
"aiCommandAllowlist"
|
|
317
|
+
],
|
|
318
|
+
).toEqual(["docker stop web"]);
|
|
319
|
+
expect(
|
|
320
|
+
validate({ aiCommandAllowlist: '["docker stop web", "docker stop *"]' })[
|
|
321
|
+
"aiCommandAllowlist"
|
|
322
|
+
],
|
|
323
|
+
).toEqual(["docker stop web", "docker stop *"]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it.each([[null], [""], [" "]])(
|
|
327
|
+
"stores a cleared allowlist (%p) as null",
|
|
328
|
+
(value: unknown) => {
|
|
329
|
+
expect(
|
|
330
|
+
validate({ aiCommandAllowlist: value })["aiCommandAllowlist"],
|
|
331
|
+
).toBe(null);
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
it("keeps an empty list as an empty list", () => {
|
|
336
|
+
expect(validate({ aiCommandAllowlist: [] })["aiCommandAllowlist"]).toEqual(
|
|
337
|
+
[],
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it.each([
|
|
342
|
+
["not JSON", "docker stop web"],
|
|
343
|
+
["an object", { pattern: "docker stop web" }],
|
|
344
|
+
["a number", 7],
|
|
345
|
+
["a JSON object", '{"a": 1}'],
|
|
346
|
+
])(
|
|
347
|
+
"refuses %s with the shape of a valid allowlist and an example for the type",
|
|
348
|
+
(_label: string, value: unknown) => {
|
|
349
|
+
expect(() => {
|
|
350
|
+
return validate({ aiCommandAllowlist: value });
|
|
351
|
+
}).toThrow(
|
|
352
|
+
'The AI command allowlist must be a JSON array of command patterns for this Docker host\'s AI agent (docker), for example ["docker stop web"].',
|
|
353
|
+
);
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
it("refuses an entry that is not text, naming its position", () => {
|
|
358
|
+
expect(() => {
|
|
359
|
+
return validate({ aiCommandAllowlist: ["docker stop web", 42] });
|
|
360
|
+
}).toThrow(
|
|
361
|
+
"Pattern 2 of the AI command allowlist must be text (got number)",
|
|
362
|
+
);
|
|
363
|
+
expect(() => {
|
|
364
|
+
return validate({ aiCommandAllowlist: [null] });
|
|
365
|
+
}).toThrow("Pattern 1 of the AI command allowlist must be text (got null)");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it(`refuses more than ${RESOURCE_ALLOWLIST_MAX_PATTERNS} patterns`, () => {
|
|
369
|
+
const patterns: Array<string> = Array.from(
|
|
370
|
+
{ length: RESOURCE_ALLOWLIST_MAX_PATTERNS + 1 },
|
|
371
|
+
(_value: unknown, index: number) => {
|
|
372
|
+
return `docker stop web-${index}`;
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(() => {
|
|
377
|
+
return validate({ aiCommandAllowlist: patterns });
|
|
378
|
+
}).toThrow(
|
|
379
|
+
`The AI command allowlist can hold at most ${RESOURCE_ALLOWLIST_MAX_PATTERNS} patterns (got ${RESOURCE_ALLOWLIST_MAX_PATTERNS + 1}).`,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(() => {
|
|
383
|
+
return validate({
|
|
384
|
+
aiCommandAllowlist: patterns.slice(0, RESOURCE_ALLOWLIST_MAX_PATTERNS),
|
|
385
|
+
});
|
|
386
|
+
}).not.toThrow();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it.each([
|
|
390
|
+
["blank", " ", "cannot be blank"],
|
|
391
|
+
["a read", "docker container inspect web", "read-only command"],
|
|
392
|
+
[
|
|
393
|
+
"another program",
|
|
394
|
+
"kubectl delete pod web",
|
|
395
|
+
"does not start with a program",
|
|
396
|
+
],
|
|
397
|
+
["too few words", "docker stop", "fewer than two words"],
|
|
398
|
+
["a * where the command goes", "docker * web", "where the command goes"],
|
|
399
|
+
["a pipe", "docker stop web | tee", "cannot be read as one command"],
|
|
400
|
+
[
|
|
401
|
+
"a Denied command",
|
|
402
|
+
"docker rm web",
|
|
403
|
+
"can never match a command that runs",
|
|
404
|
+
],
|
|
405
|
+
["too long", `docker stop ${"a".repeat(600)}`, "at most 500 characters"],
|
|
406
|
+
])(
|
|
407
|
+
"refuses %s with the policy's own reason",
|
|
408
|
+
(_label: string, pattern: string, reason: string) => {
|
|
409
|
+
let thrown: unknown = undefined;
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
validate({ aiCommandAllowlist: [pattern] });
|
|
413
|
+
} catch (error) {
|
|
414
|
+
thrown = error;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
expect(thrown).toBeInstanceOf(BadDataException);
|
|
418
|
+
expect((thrown as Error).message).toContain(
|
|
419
|
+
"Pattern 1 of the AI command allowlist cannot be used:",
|
|
420
|
+
);
|
|
421
|
+
expect((thrown as Error).message).toContain(reason);
|
|
422
|
+
expect((thrown as Error).message).toContain(
|
|
423
|
+
"* stands for exactly one whole word",
|
|
424
|
+
);
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
it("judges an entry by the resource's own type: a docker entry is no entry for a host", () => {
|
|
429
|
+
expect(() => {
|
|
430
|
+
return validate(
|
|
431
|
+
{ aiCommandAllowlist: ["docker stop web"] },
|
|
432
|
+
AiResourceType.Host,
|
|
433
|
+
);
|
|
434
|
+
}).toThrow("does not start with a program the Host AI agent runs");
|
|
435
|
+
|
|
436
|
+
expect(
|
|
437
|
+
validate(
|
|
438
|
+
{ aiCommandAllowlist: ["systemctl stop nginx"] },
|
|
439
|
+
AiResourceType.Host,
|
|
440
|
+
)["aiCommandAllowlist"],
|
|
441
|
+
).toEqual(["systemctl stop nginx"]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it.each(ALL_AI_RESOURCE_TYPES)(
|
|
445
|
+
"accepts the %s example and refuses a read of the same type",
|
|
446
|
+
(resourceType: AiResourceType) => {
|
|
447
|
+
expect(
|
|
448
|
+
validate(
|
|
449
|
+
{
|
|
450
|
+
aiCommandAllowlist: [RESOURCE_AI_ALLOWLIST_EXAMPLES[resourceType]],
|
|
451
|
+
},
|
|
452
|
+
resourceType,
|
|
453
|
+
)["aiCommandAllowlist"],
|
|
454
|
+
).toEqual([RESOURCE_AI_ALLOWLIST_EXAMPLES[resourceType]]);
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("ResourceAiAccessSettings.getLoosening", () => {
|
|
460
|
+
function loosens(
|
|
461
|
+
data: Record<string, unknown>,
|
|
462
|
+
current: Partial<ResourceAiAccessSettingsSnapshot> = {},
|
|
463
|
+
): boolean {
|
|
464
|
+
return ResourceAiAccessSettings.getLoosening({
|
|
465
|
+
data: data as JSONObject,
|
|
466
|
+
current: snapshot(current),
|
|
467
|
+
}).loosens;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
it("any move of the mode up is loosening (Off to anything, up to Automatic or Bypass approval); down never is", () => {
|
|
471
|
+
const modes: ReadonlyArray<ResourceAiRemediationMode> =
|
|
472
|
+
RESOURCE_REMEDIATION_MODES_BY_AUTONOMY;
|
|
473
|
+
|
|
474
|
+
for (let from: number = 0; from < modes.length; from++) {
|
|
475
|
+
for (let to: number = 0; to < modes.length; to++) {
|
|
476
|
+
expect({
|
|
477
|
+
from: modes[from],
|
|
478
|
+
to: modes[to],
|
|
479
|
+
loosens: loosens(
|
|
480
|
+
{ aiRemediationMode: modes[to] },
|
|
481
|
+
{ aiRemediationMode: modes[from]! },
|
|
482
|
+
),
|
|
483
|
+
}).toEqual({ from: modes[from], to: modes[to], loosens: to > from });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("adding an allowlist pattern loosens; removing, reordering or clearing does not", () => {
|
|
489
|
+
const current: Partial<ResourceAiAccessSettingsSnapshot> = {
|
|
490
|
+
aiCommandAllowlist: ["docker stop web", "docker stop api"],
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
expect(
|
|
494
|
+
loosens(
|
|
495
|
+
{ aiCommandAllowlist: ["docker stop web", "docker stop db"] },
|
|
496
|
+
current,
|
|
497
|
+
),
|
|
498
|
+
).toBe(true);
|
|
499
|
+
expect(loosens({ aiCommandAllowlist: ["docker stop api"] }, current)).toBe(
|
|
500
|
+
false,
|
|
501
|
+
);
|
|
502
|
+
expect(
|
|
503
|
+
loosens(
|
|
504
|
+
{ aiCommandAllowlist: ["docker stop api", "docker stop web"] },
|
|
505
|
+
current,
|
|
506
|
+
),
|
|
507
|
+
).toBe(false);
|
|
508
|
+
expect(loosens({ aiCommandAllowlist: [] }, current)).toBe(false);
|
|
509
|
+
expect(loosens({ aiCommandAllowlist: null }, current)).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("compares patterns trimmed, the way they are stored", () => {
|
|
513
|
+
expect(
|
|
514
|
+
loosens(
|
|
515
|
+
{ aiCommandAllowlist: [" docker stop web "] },
|
|
516
|
+
{ aiCommandAllowlist: ["docker stop web"] },
|
|
517
|
+
),
|
|
518
|
+
).toBe(false);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("negative control: the investigation switch, re-posted values and other columns loosen nothing", () => {
|
|
522
|
+
expect(loosens({ isAiInvestigationEnabled: true })).toBe(false);
|
|
523
|
+
expect(loosens({ isAiInvestigationEnabled: false })).toBe(false);
|
|
524
|
+
expect(loosens({ name: "web-1" })).toBe(false);
|
|
525
|
+
expect(
|
|
526
|
+
loosens(
|
|
527
|
+
{
|
|
528
|
+
isAiInvestigationEnabled: true,
|
|
529
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
530
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
534
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
535
|
+
},
|
|
536
|
+
),
|
|
537
|
+
).toBe(false);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe("ResourceAiAccessSettings.getLooseningRefusal", () => {
|
|
542
|
+
function refusal(
|
|
543
|
+
data: Record<string, unknown>,
|
|
544
|
+
props: DatabaseCommonInteractionProps,
|
|
545
|
+
current: Array<ResourceAiAccessSettingsSnapshot> = [snapshot()],
|
|
546
|
+
): string | null {
|
|
547
|
+
return ResourceAiAccessSettings.getLooseningRefusal({
|
|
548
|
+
resourceType: AiResourceType.DockerHost,
|
|
549
|
+
data: data as JSONObject,
|
|
550
|
+
props,
|
|
551
|
+
current,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const LOOSENING: Record<string, unknown> = {
|
|
556
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
it.each(EDITOR_ROLES)(
|
|
560
|
+
"refuses %s a loosening, in the refusal's own words",
|
|
561
|
+
(role: Permission) => {
|
|
562
|
+
expect(refusal(LOOSENING, propsWith(role))).toBe(
|
|
563
|
+
getResourceAiAccessAdminRefusal(AiResourceType.DockerHost),
|
|
564
|
+
);
|
|
565
|
+
},
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
it.each(RESOURCE_AI_ACCESS_ADMIN_PERMISSIONS)(
|
|
569
|
+
"lets %s loosen",
|
|
570
|
+
(role: Permission) => {
|
|
571
|
+
expect(refusal(LOOSENING, propsWith(role))).toBeNull();
|
|
572
|
+
},
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
it.each(EDITOR_ROLES)("lets %s tighten", (role: Permission) => {
|
|
576
|
+
expect(
|
|
577
|
+
refusal(
|
|
578
|
+
{ aiRemediationMode: ResourceAiRemediationMode.Disabled },
|
|
579
|
+
propsWith(role),
|
|
580
|
+
[snapshot({ aiRemediationMode: ResourceAiRemediationMode.Automatic })],
|
|
581
|
+
),
|
|
582
|
+
).toBeNull();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("does not read a block row as a grant", () => {
|
|
586
|
+
expect(
|
|
587
|
+
refusal(
|
|
588
|
+
LOOSENING,
|
|
589
|
+
userProps([
|
|
590
|
+
permissionRow(Permission.EditDockerHost),
|
|
591
|
+
permissionRow(Permission.ProjectAdmin, true),
|
|
592
|
+
]),
|
|
593
|
+
),
|
|
594
|
+
).not.toBeNull();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("does not count an admin grant held in another project", () => {
|
|
598
|
+
expect(
|
|
599
|
+
refusal(
|
|
600
|
+
LOOSENING,
|
|
601
|
+
userProps([permissionRow(Permission.ProjectAdmin)], {
|
|
602
|
+
tenantId: PROJECT_ID,
|
|
603
|
+
permissionProjectId: OTHER_PROJECT_ID,
|
|
604
|
+
}),
|
|
605
|
+
),
|
|
606
|
+
).not.toBeNull();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("needs the permission in EVERY matched resource's own project", () => {
|
|
610
|
+
expect(
|
|
611
|
+
refusal(
|
|
612
|
+
LOOSENING,
|
|
613
|
+
userProps([permissionRow(Permission.ProjectAdmin)], {
|
|
614
|
+
tenantId: null,
|
|
615
|
+
permissionProjectId: PROJECT_ID,
|
|
616
|
+
}),
|
|
617
|
+
[snapshot(), snapshot({ projectId: OTHER_PROJECT_ID })],
|
|
618
|
+
),
|
|
619
|
+
).not.toBeNull();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("refuses when the write loosens ANY matched resource, even if another is already there", () => {
|
|
623
|
+
expect(
|
|
624
|
+
refusal(LOOSENING, propsWith(Permission.SettingsMember), [
|
|
625
|
+
snapshot({
|
|
626
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
627
|
+
}),
|
|
628
|
+
snapshot({
|
|
629
|
+
aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
|
|
630
|
+
}),
|
|
631
|
+
]),
|
|
632
|
+
).not.toBeNull();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("judges a write that matched nothing against the never-configured defaults in the caller's tenant", () => {
|
|
636
|
+
expect(
|
|
637
|
+
refusal(LOOSENING, propsWith(Permission.SettingsMember), []),
|
|
638
|
+
).not.toBeNull();
|
|
639
|
+
expect(
|
|
640
|
+
refusal(LOOSENING, propsWith(Permission.ProjectAdmin), []),
|
|
641
|
+
).toBeNull();
|
|
642
|
+
expect(
|
|
643
|
+
refusal(
|
|
644
|
+
{ aiRemediationMode: ResourceAiRemediationMode.Disabled },
|
|
645
|
+
propsWith(Permission.SettingsMember),
|
|
646
|
+
[],
|
|
647
|
+
),
|
|
648
|
+
).toBeNull();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("fails closed when nothing matched and the caller has no tenant", () => {
|
|
652
|
+
expect(
|
|
653
|
+
refusal(
|
|
654
|
+
LOOSENING,
|
|
655
|
+
userProps([permissionRow(Permission.ProjectAdmin)], {
|
|
656
|
+
tenantId: null,
|
|
657
|
+
permissionProjectId: PROJECT_ID,
|
|
658
|
+
}),
|
|
659
|
+
[],
|
|
660
|
+
),
|
|
661
|
+
).not.toBeNull();
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe("ResourceAiAccessSettings.getServerOnlyColumnRefusal", () => {
|
|
666
|
+
it.each([...RESOURCE_AI_ACCESS_SERVER_ONLY_KEYS])(
|
|
667
|
+
"refuses a write of %s, a null clear included",
|
|
668
|
+
(key: string) => {
|
|
669
|
+
for (const value of [new Date(), "x", null]) {
|
|
670
|
+
expect(
|
|
671
|
+
ResourceAiAccessSettings.getServerOnlyColumnRefusal({
|
|
672
|
+
[key]: value,
|
|
673
|
+
} as JSONObject),
|
|
674
|
+
).toBe(
|
|
675
|
+
`${key} is written by OneUptime itself and cannot be set through the API.`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
it("names every server-only column a write carries", () => {
|
|
682
|
+
expect(
|
|
683
|
+
ResourceAiAccessSettings.getServerOnlyColumnRefusal({
|
|
684
|
+
aiAccessConfiguredAt: new Date(),
|
|
685
|
+
aiAccessLastError: "boom",
|
|
686
|
+
} as unknown as JSONObject),
|
|
687
|
+
).toBe(
|
|
688
|
+
"aiAccessConfiguredAt, aiAccessLastError are written by OneUptime itself and cannot be set through the API.",
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("negative control: settings and other columns are not refused", () => {
|
|
693
|
+
expect(
|
|
694
|
+
ResourceAiAccessSettings.getServerOnlyColumnRefusal({
|
|
695
|
+
isAiInvestigationEnabled: true,
|
|
696
|
+
aiRemediationMode: ResourceAiRemediationMode.Automatic,
|
|
697
|
+
name: "web",
|
|
698
|
+
} as JSONObject),
|
|
699
|
+
).toBeNull();
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("ResourceAiAccessSettings.readStoredMode / readStoredAllowlist", () => {
|
|
704
|
+
it("reads an unknown mode as Disabled", () => {
|
|
705
|
+
for (const value of [undefined, null, "automatic", 3, {}]) {
|
|
706
|
+
expect(ResourceAiAccessSettings.readStoredMode(value)).toBe(
|
|
707
|
+
ResourceAiRemediationMode.Disabled,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
expect(
|
|
711
|
+
ResourceAiAccessSettings.readStoredMode(
|
|
712
|
+
ResourceAiRemediationMode.Automatic,
|
|
713
|
+
),
|
|
714
|
+
).toBe(ResourceAiRemediationMode.Automatic);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("reads trimmed non-empty strings, a JSON string, and nothing else", () => {
|
|
718
|
+
expect(
|
|
719
|
+
ResourceAiAccessSettings.readStoredAllowlist([
|
|
720
|
+
" docker stop web ",
|
|
721
|
+
"",
|
|
722
|
+
7,
|
|
723
|
+
null,
|
|
724
|
+
]),
|
|
725
|
+
).toEqual(["docker stop web"]);
|
|
726
|
+
expect(
|
|
727
|
+
ResourceAiAccessSettings.readStoredAllowlist('["docker stop web"]'),
|
|
728
|
+
).toEqual(["docker stop web"]);
|
|
729
|
+
expect(ResourceAiAccessSettings.readStoredAllowlist(null)).toEqual([]);
|
|
730
|
+
expect(ResourceAiAccessSettings.readStoredAllowlist({})).toEqual([]);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
describe("ResourceAiAccessSettings.checkUpdate", () => {
|
|
735
|
+
let service: FakeService;
|
|
736
|
+
|
|
737
|
+
beforeEach(() => {
|
|
738
|
+
service = fakeService([row()]);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
function check(
|
|
742
|
+
data: Record<string, unknown>,
|
|
743
|
+
props: DatabaseCommonInteractionProps,
|
|
744
|
+
resourceType: AiResourceType = AiResourceType.DockerHost,
|
|
745
|
+
): Promise<ResourceAiAccessWriteCarryForward | null> {
|
|
746
|
+
return ResourceAiAccessSettings.checkUpdate({
|
|
747
|
+
resourceType,
|
|
748
|
+
service: asService(service),
|
|
749
|
+
updateBy: updateBy(data, props),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
it("refuses an editor's loosening before anything is written, and reads the settings scoped to the tenant as root", async () => {
|
|
754
|
+
await expect(
|
|
755
|
+
check(
|
|
756
|
+
{ aiRemediationMode: ResourceAiRemediationMode.RequireApproval },
|
|
757
|
+
propsWith(Permission.SettingsMember),
|
|
758
|
+
),
|
|
759
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
760
|
+
|
|
761
|
+
expect(service.findBy).toHaveBeenCalledTimes(1);
|
|
762
|
+
const args: {
|
|
763
|
+
query: Record<string, unknown>;
|
|
764
|
+
select: Record<string, unknown>;
|
|
765
|
+
props: DatabaseCommonInteractionProps;
|
|
766
|
+
} = service.findBy.mock.calls[0]![0] as {
|
|
767
|
+
query: Record<string, unknown>;
|
|
768
|
+
select: Record<string, unknown>;
|
|
769
|
+
props: DatabaseCommonInteractionProps;
|
|
770
|
+
};
|
|
771
|
+
expect(args.query).toEqual({
|
|
772
|
+
_id: RESOURCE_ID.toString(),
|
|
773
|
+
projectId: PROJECT_ID,
|
|
774
|
+
});
|
|
775
|
+
expect(args.props).toEqual({ isRoot: true });
|
|
776
|
+
for (const column of [
|
|
777
|
+
"projectId",
|
|
778
|
+
"isAiInvestigationEnabled",
|
|
779
|
+
"aiRemediationMode",
|
|
780
|
+
"aiCommandAllowlist",
|
|
781
|
+
]) {
|
|
782
|
+
expect(args.select[column]).toBe(true);
|
|
783
|
+
}
|
|
784
|
+
expect(service.updateBy).not.toHaveBeenCalled();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it("hands an admin's write the settings as they stood, for the marker and the feed", async () => {
|
|
788
|
+
service.findBy.mockResolvedValue([
|
|
789
|
+
row({
|
|
790
|
+
isAiInvestigationEnabled: true,
|
|
791
|
+
aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
|
|
792
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
793
|
+
}),
|
|
794
|
+
]);
|
|
795
|
+
|
|
796
|
+
const carry: ResourceAiAccessWriteCarryForward | null = await check(
|
|
797
|
+
{ aiRemediationMode: ResourceAiRemediationMode.BypassApproval },
|
|
798
|
+
propsWith(Permission.EditAutoRemediationRule),
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
expect(carry).toEqual({
|
|
802
|
+
resourceType: AiResourceType.DockerHost,
|
|
803
|
+
previousResourceAiAccessSettings: {
|
|
804
|
+
[RESOURCE_ID.toString()]: {
|
|
805
|
+
projectId: PROJECT_ID,
|
|
806
|
+
isAiInvestigationEnabled: true,
|
|
807
|
+
aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
|
|
808
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it("reads an unknown stored mode as Disabled, so moving to Automatic from it is a loosening", async () => {
|
|
815
|
+
service.findBy.mockResolvedValue([row({ aiRemediationMode: "automatic" })]);
|
|
816
|
+
|
|
817
|
+
await expect(
|
|
818
|
+
check(
|
|
819
|
+
{ aiRemediationMode: ResourceAiRemediationMode.Automatic },
|
|
820
|
+
propsWith(Permission.SettingsMember),
|
|
821
|
+
),
|
|
822
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("lets an editor re-post every unchanged setting while flipping investigation", async () => {
|
|
826
|
+
service.findBy.mockResolvedValue([
|
|
827
|
+
row({
|
|
828
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
829
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
830
|
+
}),
|
|
831
|
+
]);
|
|
832
|
+
|
|
833
|
+
await expect(
|
|
834
|
+
check(
|
|
835
|
+
{
|
|
836
|
+
isAiInvestigationEnabled: true,
|
|
837
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
838
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
839
|
+
},
|
|
840
|
+
propsWith(Permission.EditDockerHost),
|
|
841
|
+
),
|
|
842
|
+
).resolves.not.toBeNull();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("returns null (no read, no marker) for a write that carries no AI setting", async () => {
|
|
846
|
+
await expect(
|
|
847
|
+
check({ name: "web-2" }, propsWith(Permission.SettingsMember)),
|
|
848
|
+
).resolves.toBeNull();
|
|
849
|
+
expect(service.findBy).not.toHaveBeenCalled();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("validates before who is asking: an invalid mode is a bad request for an admin too", async () => {
|
|
853
|
+
await expect(
|
|
854
|
+
check(
|
|
855
|
+
{ aiRemediationMode: "Everything" },
|
|
856
|
+
propsWith(Permission.ProjectOwner),
|
|
857
|
+
),
|
|
858
|
+
).rejects.toThrow(BadDataException);
|
|
859
|
+
expect(service.findBy).not.toHaveBeenCalled();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it("refuses the server-only columns for every caller but root — a master admin included", async () => {
|
|
863
|
+
for (const props of [
|
|
864
|
+
propsWith(Permission.ProjectOwner),
|
|
865
|
+
{ isMasterAdmin: true, userId: ObjectID.generate() },
|
|
866
|
+
] as Array<DatabaseCommonInteractionProps>) {
|
|
867
|
+
await expect(
|
|
868
|
+
check({ aiAccessConfiguredAt: new Date() }, props),
|
|
869
|
+
).rejects.toThrow(
|
|
870
|
+
"aiAccessConfiguredAt is written by OneUptime itself and cannot be set through the API.",
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
await expect(
|
|
875
|
+
check(
|
|
876
|
+
{ aiAccessLastVerifiedAt: new Date(), aiAccessLastError: null },
|
|
877
|
+
{ isRoot: true },
|
|
878
|
+
),
|
|
879
|
+
).resolves.toBeNull();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it("lets a master admin loosen without any project permission, and still hands the snapshot forward", async () => {
|
|
883
|
+
await expect(
|
|
884
|
+
check(
|
|
885
|
+
{ aiRemediationMode: ResourceAiRemediationMode.BypassApproval },
|
|
886
|
+
{ isMasterAdmin: true, userId: ObjectID.generate() },
|
|
887
|
+
),
|
|
888
|
+
).resolves.not.toBeNull();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("never gates, reads or marks the server's own (root) writes, but still validates them", async () => {
|
|
892
|
+
await expect(
|
|
893
|
+
check(
|
|
894
|
+
{
|
|
895
|
+
isAiInvestigationEnabled: true,
|
|
896
|
+
aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
|
|
897
|
+
},
|
|
898
|
+
{ isRoot: true },
|
|
899
|
+
),
|
|
900
|
+
).resolves.toBeNull();
|
|
901
|
+
expect(service.findBy).not.toHaveBeenCalled();
|
|
902
|
+
|
|
903
|
+
await expect(
|
|
904
|
+
check({ aiRemediationMode: "nope" }, { isRoot: true }),
|
|
905
|
+
).rejects.toThrow(BadDataException);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("normalizes the allowlist in the write itself", async () => {
|
|
909
|
+
const write: UpdateBy<BaseModel> = updateBy(
|
|
910
|
+
{ aiCommandAllowlist: '[" docker stop web "]' },
|
|
911
|
+
propsWith(Permission.ProjectAdmin),
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
await ResourceAiAccessSettings.checkUpdate({
|
|
915
|
+
resourceType: AiResourceType.DockerHost,
|
|
916
|
+
service: asService(service),
|
|
917
|
+
updateBy: write,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
expect((write.data as unknown as JSONObject)["aiCommandAllowlist"]).toEqual(
|
|
921
|
+
["docker stop web"],
|
|
922
|
+
);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("an update that reaches several resources is judged per resource", async () => {
|
|
926
|
+
service.findBy.mockResolvedValue([
|
|
927
|
+
row({ aiRemediationMode: ResourceAiRemediationMode.BypassApproval }),
|
|
928
|
+
row({
|
|
929
|
+
id: OTHER_RESOURCE_ID,
|
|
930
|
+
_id: OTHER_RESOURCE_ID.toString(),
|
|
931
|
+
aiRemediationMode: ResourceAiRemediationMode.Disabled,
|
|
932
|
+
}),
|
|
933
|
+
]);
|
|
934
|
+
|
|
935
|
+
await expect(
|
|
936
|
+
check(
|
|
937
|
+
{ aiRemediationMode: ResourceAiRemediationMode.BypassApproval },
|
|
938
|
+
propsWith(Permission.SettingsMember),
|
|
939
|
+
),
|
|
940
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe("ResourceAiAccessSettings.afterUpdate", () => {
|
|
945
|
+
let service: FakeService;
|
|
946
|
+
let feedItems: Array<ResourceAiAccessFeedItem>;
|
|
947
|
+
let createFeedItem: jest.Mock;
|
|
948
|
+
let getResourceMarkdownLink: jest.Mock;
|
|
949
|
+
|
|
950
|
+
beforeEach(() => {
|
|
951
|
+
service = fakeService();
|
|
952
|
+
feedItems = [];
|
|
953
|
+
createFeedItem = jest
|
|
954
|
+
.fn()
|
|
955
|
+
.mockImplementation(async (item: ResourceAiAccessFeedItem) => {
|
|
956
|
+
feedItems.push(item);
|
|
957
|
+
});
|
|
958
|
+
getResourceMarkdownLink = jest
|
|
959
|
+
.fn()
|
|
960
|
+
.mockResolvedValue("[Docker Host web-1](https://x)");
|
|
961
|
+
jest
|
|
962
|
+
.spyOn(UserService, "getUserMarkdownString")
|
|
963
|
+
.mockResolvedValue("[Jane](https://oneuptime.example/user)");
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
afterEach(() => {
|
|
967
|
+
jest.restoreAllMocks();
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
971
|
+
for (let i: number = 0; i < 100 && !predicate(); i++) {
|
|
972
|
+
await new Promise((resolve: (value: unknown) => void) => {
|
|
973
|
+
setTimeout(resolve, 0);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function onUpdate(
|
|
979
|
+
data: Record<string, unknown>,
|
|
980
|
+
carryForward: unknown,
|
|
981
|
+
props: DatabaseCommonInteractionProps = propsWith(Permission.ProjectAdmin),
|
|
982
|
+
): OnUpdate<BaseModel> {
|
|
983
|
+
return {
|
|
984
|
+
updateBy: updateBy(data, props),
|
|
985
|
+
carryForward,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function carry(
|
|
990
|
+
previous: Partial<ResourceAiAccessSettingsSnapshot> = {},
|
|
991
|
+
): ResourceAiAccessWriteCarryForward {
|
|
992
|
+
return {
|
|
993
|
+
resourceType: AiResourceType.DockerHost,
|
|
994
|
+
previousResourceAiAccessSettings: {
|
|
995
|
+
[RESOURCE_ID.toString()]: snapshot(previous),
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function after(
|
|
1001
|
+
update: OnUpdate<BaseModel>,
|
|
1002
|
+
ids: Array<ObjectID> = [RESOURCE_ID],
|
|
1003
|
+
): Promise<void> {
|
|
1004
|
+
await ResourceAiAccessSettings.afterUpdate({
|
|
1005
|
+
service: asService(service),
|
|
1006
|
+
onUpdate: update,
|
|
1007
|
+
updatedItemIds: ids,
|
|
1008
|
+
getResourceMarkdownLink,
|
|
1009
|
+
createFeedItem,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
it("marks the resource AI-configured, only where it is not yet, as root", async () => {
|
|
1014
|
+
await after(
|
|
1015
|
+
onUpdate(
|
|
1016
|
+
{ aiRemediationMode: ResourceAiRemediationMode.RequireApproval },
|
|
1017
|
+
carry(),
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
expect(service.updateBy).toHaveBeenCalledTimes(1);
|
|
1022
|
+
const args: {
|
|
1023
|
+
query: Record<string, unknown>;
|
|
1024
|
+
data: Record<string, unknown>;
|
|
1025
|
+
props: DatabaseCommonInteractionProps;
|
|
1026
|
+
} = service.updateBy.mock.calls[0]![0] as {
|
|
1027
|
+
query: Record<string, unknown>;
|
|
1028
|
+
data: Record<string, unknown>;
|
|
1029
|
+
props: DatabaseCommonInteractionProps;
|
|
1030
|
+
};
|
|
1031
|
+
expect(args.data["aiAccessConfiguredAt"]).toBeInstanceOf(Date);
|
|
1032
|
+
expect(args.query["aiAccessConfiguredAt"]).toBeDefined();
|
|
1033
|
+
expect(args.query["_id"]).toBeDefined();
|
|
1034
|
+
expect(args.props).toEqual({ isRoot: true });
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it("records who changed what, old to new, yellow for a loosening", async () => {
|
|
1038
|
+
const props: DatabaseCommonInteractionProps = propsWith(
|
|
1039
|
+
Permission.ProjectAdmin,
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await after(
|
|
1043
|
+
onUpdate(
|
|
1044
|
+
{
|
|
1045
|
+
aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
|
|
1046
|
+
aiCommandAllowlist: ["docker stop web"],
|
|
1047
|
+
},
|
|
1048
|
+
carry({ aiRemediationMode: ResourceAiRemediationMode.RequireApproval }),
|
|
1049
|
+
props,
|
|
1050
|
+
),
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
await waitFor(() => {
|
|
1054
|
+
return feedItems.length > 0;
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
expect(feedItems).toHaveLength(1);
|
|
1058
|
+
const item: ResourceAiAccessFeedItem = feedItems[0]!;
|
|
1059
|
+
expect(item.resourceId).toBe(RESOURCE_ID);
|
|
1060
|
+
expect(item.projectId).toBe(PROJECT_ID);
|
|
1061
|
+
expect(item.userId).toBe(props.userId);
|
|
1062
|
+
expect(item.displayColor).toBe(Yellow500);
|
|
1063
|
+
expect(item.feedInfoInMarkdown).toBe(
|
|
1064
|
+
"🤖 **[Jane](https://oneuptime.example/user)** changed what OneUptime AI may do on [Docker Host web-1](https://x):\n\n- AI remediation changed from **Ask for approval** to **Bypass approval**\n- AI command allowlist changed to 1 pattern (was none)",
|
|
1065
|
+
);
|
|
1066
|
+
expect(item.moreInformationInMarkdown).toContain(
|
|
1067
|
+
"**Changed by**: [Jane](https://oneuptime.example/user)",
|
|
1068
|
+
);
|
|
1069
|
+
expect(item.moreInformationInMarkdown).toContain("- `docker stop web`");
|
|
1070
|
+
expect(getResourceMarkdownLink).toHaveBeenCalledWith(
|
|
1071
|
+
PROJECT_ID,
|
|
1072
|
+
RESOURCE_ID,
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("is grey for a tightening, and attributes a write with no user to an API key", async () => {
|
|
1077
|
+
await after(
|
|
1078
|
+
onUpdate(
|
|
1079
|
+
{ aiRemediationMode: ResourceAiRemediationMode.Disabled },
|
|
1080
|
+
carry({ aiRemediationMode: ResourceAiRemediationMode.Automatic }),
|
|
1081
|
+
{ tenantId: PROJECT_ID } as DatabaseCommonInteractionProps,
|
|
1082
|
+
),
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
await waitFor(() => {
|
|
1086
|
+
return feedItems.length > 0;
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(feedItems[0]!.displayColor).toBe(Gray500);
|
|
1090
|
+
expect(feedItems[0]!.feedInfoInMarkdown).toContain(
|
|
1091
|
+
"🤖 An API key changed what OneUptime AI may do on",
|
|
1092
|
+
);
|
|
1093
|
+
expect(feedItems[0]!.feedInfoInMarkdown).toContain(
|
|
1094
|
+
"AI remediation changed from **Automatic** to **Off**",
|
|
1095
|
+
);
|
|
1096
|
+
expect(feedItems[0]!.moreInformationInMarkdown).toBe(
|
|
1097
|
+
"**Changed by**: An API key (no user)",
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it("records nothing for a save that re-posted unchanged values, but still marks the resource configured", async () => {
|
|
1102
|
+
await after(
|
|
1103
|
+
onUpdate(
|
|
1104
|
+
{
|
|
1105
|
+
isAiInvestigationEnabled: false,
|
|
1106
|
+
aiRemediationMode: ResourceAiRemediationMode.Disabled,
|
|
1107
|
+
aiCommandAllowlist: [],
|
|
1108
|
+
},
|
|
1109
|
+
carry(),
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
for (let i: number = 0; i < 10; i++) {
|
|
1114
|
+
await new Promise((resolve: (value: unknown) => void) => {
|
|
1115
|
+
setTimeout(resolve, 0);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
expect(createFeedItem).not.toHaveBeenCalled();
|
|
1120
|
+
expect(service.updateBy).toHaveBeenCalledTimes(1);
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it("does nothing without the carry-forward (the server's own writes, other columns)", async () => {
|
|
1124
|
+
await after(onUpdate({ name: "web" }, null));
|
|
1125
|
+
await after(onUpdate({ name: "web" }, []));
|
|
1126
|
+
|
|
1127
|
+
expect(service.updateBy).not.toHaveBeenCalled();
|
|
1128
|
+
expect(createFeedItem).not.toHaveBeenCalled();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("does nothing when the update touched no row", async () => {
|
|
1132
|
+
await after(
|
|
1133
|
+
onUpdate(
|
|
1134
|
+
{ aiRemediationMode: ResourceAiRemediationMode.RequireApproval },
|
|
1135
|
+
carry(),
|
|
1136
|
+
),
|
|
1137
|
+
[],
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
expect(service.updateBy).not.toHaveBeenCalled();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("a feed write that fails never fails the save", async () => {
|
|
1144
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
1145
|
+
return undefined;
|
|
1146
|
+
});
|
|
1147
|
+
createFeedItem.mockRejectedValue(new Error("feed down"));
|
|
1148
|
+
|
|
1149
|
+
await expect(
|
|
1150
|
+
after(onUpdate({ isAiInvestigationEnabled: true }, carry())),
|
|
1151
|
+
).resolves.toBeUndefined();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it("a marker write that fails fails the save (a registering agent reads it)", async () => {
|
|
1155
|
+
service.updateBy.mockRejectedValue(new Error("db down"));
|
|
1156
|
+
|
|
1157
|
+
await expect(
|
|
1158
|
+
after(onUpdate({ isAiInvestigationEnabled: true }, carry())),
|
|
1159
|
+
).rejects.toThrow("db down");
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("looks the project up when the resource was not in the snapshot", async () => {
|
|
1163
|
+
await after(
|
|
1164
|
+
onUpdate(
|
|
1165
|
+
{ isAiInvestigationEnabled: true },
|
|
1166
|
+
{
|
|
1167
|
+
resourceType: AiResourceType.DockerHost,
|
|
1168
|
+
previousResourceAiAccessSettings: {},
|
|
1169
|
+
},
|
|
1170
|
+
),
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
await waitFor(() => {
|
|
1174
|
+
return feedItems.length > 0;
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
expect(service.findOneById).toHaveBeenCalledTimes(1);
|
|
1178
|
+
expect(feedItems[0]!.feedInfoInMarkdown).toContain(
|
|
1179
|
+
"AI investigation with read-only commands turned **on**",
|
|
1180
|
+
);
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
describe("ResourceAiAccessSettings.describeChanges", () => {
|
|
1185
|
+
it("reports only what changed, old to new", () => {
|
|
1186
|
+
expect(
|
|
1187
|
+
ResourceAiAccessSettings.describeChanges({
|
|
1188
|
+
updateData: {
|
|
1189
|
+
isAiInvestigationEnabled: true,
|
|
1190
|
+
aiRemediationMode: ResourceAiRemediationMode.Automatic,
|
|
1191
|
+
aiCommandAllowlist: null,
|
|
1192
|
+
} as JSONObject,
|
|
1193
|
+
previous: snapshot({
|
|
1194
|
+
aiRemediationMode: ResourceAiRemediationMode.Automatic,
|
|
1195
|
+
aiCommandAllowlist: ["docker stop web", "docker stop api"],
|
|
1196
|
+
}),
|
|
1197
|
+
}),
|
|
1198
|
+
).toEqual([
|
|
1199
|
+
"AI investigation with read-only commands turned **on**",
|
|
1200
|
+
"AI command allowlist cleared",
|
|
1201
|
+
]);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it("without a previous snapshot reports every written setting as set", () => {
|
|
1205
|
+
expect(
|
|
1206
|
+
ResourceAiAccessSettings.describeChanges({
|
|
1207
|
+
updateData: {
|
|
1208
|
+
isAiInvestigationEnabled: false,
|
|
1209
|
+
aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
|
|
1210
|
+
aiCommandAllowlist: ["docker stop web", "docker stop api"],
|
|
1211
|
+
} as JSONObject,
|
|
1212
|
+
previous: undefined,
|
|
1213
|
+
}),
|
|
1214
|
+
).toEqual([
|
|
1215
|
+
"AI investigation with read-only commands turned **off**",
|
|
1216
|
+
"AI remediation set to **Ask for approval**",
|
|
1217
|
+
"AI command allowlist changed to 2 patterns",
|
|
1218
|
+
]);
|
|
1219
|
+
});
|
|
1220
|
+
});
|