@oneuptime/common 14.0.9 → 14.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,1305 @@
1
+ import ResourceCommandPolicy, {
2
+ RESOURCE_ALLOWLIST_MAX_PATTERNS,
3
+ RESOURCE_ALLOWLIST_MAX_PATTERN_LENGTH,
4
+ } from "../../../../Utils/AiRemediation/Resource/ResourceCommandPolicy";
5
+ import {
6
+ ResourceAutoExecutionVerdict,
7
+ ResourceCommandPolicyResult,
8
+ ResourceToolPolicy,
9
+ deniedResult,
10
+ renderResourceDisplayCommand,
11
+ } from "../../../../Utils/AiRemediation/Resource/ResourceCommandPolicyCore";
12
+ import DockerEngineCommandPolicy from "../../../../Utils/AiRemediation/Resource/DockerEngineCommandPolicy";
13
+ import DockerSwarmCommandPolicy from "../../../../Utils/AiRemediation/Resource/DockerSwarmCommandPolicy";
14
+ import ProxmoxCommandPolicy from "../../../../Utils/AiRemediation/Resource/ProxmoxCommandPolicy";
15
+ import GovcCommandPolicy from "../../../../Utils/AiRemediation/Resource/GovcCommandPolicy";
16
+ import CephCommandPolicy from "../../../../Utils/AiRemediation/Resource/CephCommandPolicy";
17
+ import DatabaseCommandPolicy from "../../../../Utils/AiRemediation/Resource/DatabaseCommandPolicy";
18
+ import HostCommandPolicy from "../../../../Utils/AiRemediation/Resource/HostCommandPolicy";
19
+ import AiResourceType, {
20
+ AI_RESOURCE_TYPE_INFO,
21
+ ALL_AI_RESOURCE_TYPES,
22
+ } from "../../../../Types/ResourceAiAgent/AiResourceType";
23
+ import { ResourceCommandTier } from "../../../../Types/ResourceAiAgent/ResourceAiAccess";
24
+ import { AiRemediationCommandPolicyVerdict } from "../../../../Types/AutoRemediation/AiRemediationCommandPolicyVerdict";
25
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
26
+
27
+ /*
28
+ * Contract under test — ResourceCommandPolicy, the dispatcher every caller
29
+ * uses (server tools, the enqueue chokepoint, the approval path, the
30
+ * dashboard form and the agent).
31
+ *
32
+ * - Routing: each resource type reaches its tool policy; a command whose
33
+ * first word is not one of the type's programs is Denied before any tool
34
+ * policy reads it; the Wave 1 stubs deny everything.
35
+ * - Fail-closed argv handling: garbage, oversized, multi-line and
36
+ * privilege-escalating argvs are Denied without reaching the tool, and a
37
+ * tool that throws or answers nonsense is read as Denied.
38
+ * - The ladder, the allowlist (validity, matching, broadness) and the
39
+ * agent's write scope, exercised against a fake tool policy with a real
40
+ * grammar so every branch is reachable while the stubs deny everything.
41
+ */
42
+
43
+ // A number, as a memory limit or an OSD id is written.
44
+ const DIGITS: RegExp = /^[0-9]+$/;
45
+
46
+ /*
47
+ * A tiny docker-like grammar: enough shapes to reach every branch of the
48
+ * dispatcher. The real kits replace the stubs; this never ships.
49
+ */
50
+ const FAKE_DOCKER: ResourceToolPolicy = {
51
+ name: "fake-docker",
52
+ programs: ["docker"],
53
+ readCommandGuide: "- docker ps",
54
+ writeCommandGuide: "- docker restart NAME (safe)",
55
+ evaluateArgv(argv: Array<string>): ResourceCommandPolicyResult {
56
+ const [program, verb, ...rest] = argv;
57
+ const base: Omit<ResourceCommandPolicyResult, "tier" | "reason"> = {
58
+ program: program || "",
59
+ args: argv.slice(1),
60
+ verb: verb || "",
61
+ displayCommand: renderResourceDisplayCommand(argv),
62
+ targets: [],
63
+ };
64
+
65
+ if (verb === "ps" && rest.length <= 1) {
66
+ return {
67
+ ...base,
68
+ tier: ResourceCommandTier.Read,
69
+ reason: "lists containers",
70
+ };
71
+ }
72
+
73
+ if (verb === "restart" && rest.length === 1) {
74
+ return {
75
+ ...base,
76
+ tier: ResourceCommandTier.SafeWrite,
77
+ reason: "restarts one container",
78
+ targets: [rest[0]!],
79
+ };
80
+ }
81
+
82
+ if (verb === "stop" && rest.length === 1) {
83
+ return {
84
+ ...base,
85
+ tier: ResourceCommandTier.RiskyWrite,
86
+ reason: "stops one container",
87
+ targets: [rest[0]!],
88
+ };
89
+ }
90
+
91
+ if (verb === "kill" && rest.length === 1) {
92
+ return {
93
+ ...base,
94
+ tier: ResourceCommandTier.RiskyWrite,
95
+ reason: "kills one container",
96
+ targets: [rest[0]!],
97
+ requiresHuman: true,
98
+ };
99
+ }
100
+
101
+ if (
102
+ verb === "update" &&
103
+ rest.length === 3 &&
104
+ rest[0] === "--memory" &&
105
+ DIGITS.test(rest[1]!)
106
+ ) {
107
+ return {
108
+ ...base,
109
+ tier: ResourceCommandTier.RiskyWrite,
110
+ reason: "changes one container's memory limit",
111
+ targets: [rest[2]!],
112
+ };
113
+ }
114
+
115
+ if (verb === "system" && rest[0] === "prune" && rest.length === 1) {
116
+ return {
117
+ ...base,
118
+ tier: ResourceCommandTier.RiskyWrite,
119
+ reason: "prunes the engine",
120
+ targets: [],
121
+ };
122
+ }
123
+
124
+ return deniedResult(argv, `docker ${verb || ""} is not allowed`);
125
+ },
126
+ };
127
+
128
+ // A ceph-like grammar whose target must be a number (an OSD id).
129
+ const FAKE_CEPH: ResourceToolPolicy = {
130
+ name: "fake-ceph",
131
+ programs: ["ceph"],
132
+ readCommandGuide: "- ceph health",
133
+ writeCommandGuide: "- ceph osd out ID (risky)",
134
+ evaluateArgv(argv: Array<string>): ResourceCommandPolicyResult {
135
+ if (argv[1] === "health" && argv.length === 2) {
136
+ return {
137
+ tier: ResourceCommandTier.Read,
138
+ reason: "shows health",
139
+ program: "ceph",
140
+ args: argv.slice(1),
141
+ verb: "health",
142
+ displayCommand: renderResourceDisplayCommand(argv),
143
+ targets: [],
144
+ };
145
+ }
146
+
147
+ if (
148
+ argv[1] === "osd" &&
149
+ argv[2] === "out" &&
150
+ argv.length === 4 &&
151
+ DIGITS.test(argv[3]!)
152
+ ) {
153
+ return {
154
+ tier: ResourceCommandTier.RiskyWrite,
155
+ reason: "marks one OSD out",
156
+ program: "ceph",
157
+ args: argv.slice(1),
158
+ verb: "osd out",
159
+ displayCommand: renderResourceDisplayCommand(argv),
160
+ targets: [`osd.${argv[3]}`],
161
+ };
162
+ }
163
+
164
+ return deniedResult(argv, "not a ceph command this fake knows");
165
+ },
166
+ };
167
+
168
+ function useFakeTools(): jest.SpyInstance {
169
+ return jest
170
+ .spyOn(ResourceCommandPolicy, "getToolPolicy")
171
+ .mockImplementation((type: AiResourceType): ResourceToolPolicy => {
172
+ return type === AiResourceType.CephCluster ? FAKE_CEPH : FAKE_DOCKER;
173
+ });
174
+ }
175
+
176
+ function evaluate(
177
+ command: string,
178
+ resourceType: AiResourceType = AiResourceType.DockerHost,
179
+ ): ResourceCommandPolicyResult {
180
+ return ResourceCommandPolicy.evaluateCommand({ resourceType, command });
181
+ }
182
+
183
+ afterEach(() => {
184
+ jest.restoreAllMocks();
185
+ });
186
+
187
+ describe("routing", () => {
188
+ test("each resource type reaches its tool policy", () => {
189
+ const expected: Record<AiResourceType, ResourceToolPolicy> = {
190
+ [AiResourceType.DockerHost]: DockerEngineCommandPolicy,
191
+ [AiResourceType.PodmanHost]: DockerEngineCommandPolicy,
192
+ [AiResourceType.DockerSwarmCluster]: DockerSwarmCommandPolicy,
193
+ [AiResourceType.ProxmoxCluster]: ProxmoxCommandPolicy,
194
+ [AiResourceType.VMwareVCenter]: GovcCommandPolicy,
195
+ [AiResourceType.CephCluster]: CephCommandPolicy,
196
+ [AiResourceType.DatabaseServer]: DatabaseCommandPolicy,
197
+ [AiResourceType.Host]: HostCommandPolicy,
198
+ };
199
+
200
+ for (const type of ALL_AI_RESOURCE_TYPES) {
201
+ expect(ResourceCommandPolicy.getToolPolicy(type)).toBe(expected[type]);
202
+ }
203
+ });
204
+
205
+ test("pins the tool policy names", () => {
206
+ expect(
207
+ ALL_AI_RESOURCE_TYPES.map((type: AiResourceType): string => {
208
+ return ResourceCommandPolicy.getToolPolicy(type).name;
209
+ }),
210
+ ).toEqual([
211
+ "docker-engine",
212
+ "docker-engine",
213
+ "docker-swarm",
214
+ "pvesh",
215
+ "govc",
216
+ "ceph",
217
+ "db",
218
+ "host",
219
+ ]);
220
+ });
221
+
222
+ test("each tool policy covers exactly its types' programs", () => {
223
+ for (const type of ALL_AI_RESOURCE_TYPES) {
224
+ expect([...ResourceCommandPolicy.getToolPolicy(type).programs]).toEqual([
225
+ ...AI_RESOURCE_TYPE_INFO[type].programs,
226
+ ]);
227
+ }
228
+ });
229
+
230
+ test("an unknown type gets a policy that refuses everything", () => {
231
+ const tool: ResourceToolPolicy = ResourceCommandPolicy.getToolPolicy(
232
+ "KubernetesCluster" as AiResourceType,
233
+ );
234
+
235
+ expect(tool.programs).toEqual([]);
236
+ expect(tool.evaluateArgv(["kubectl", "get", "pods"]).tier).toBe(
237
+ ResourceCommandTier.Denied,
238
+ );
239
+ expect(
240
+ evaluate("kubectl get pods", "KubernetesCluster" as AiResourceType).tier,
241
+ ).toBe(ResourceCommandTier.Denied);
242
+ expect(
243
+ evaluate("docker ps", "KubernetesCluster" as AiResourceType).reason,
244
+ ).toContain("no command policy");
245
+ });
246
+ });
247
+
248
+ describe("the Wave 1 stubs fail closed", () => {
249
+ const stubs: Array<ResourceToolPolicy> = [
250
+ DockerEngineCommandPolicy,
251
+ DockerSwarmCommandPolicy,
252
+ ProxmoxCommandPolicy,
253
+ GovcCommandPolicy,
254
+ CephCommandPolicy,
255
+ DatabaseCommandPolicy,
256
+ HostCommandPolicy,
257
+ ];
258
+
259
+ /*
260
+ * Only while a stub is in place: a kit that replaces the module makes its
261
+ * own reads non-Denied, and these assertions move to the kit's suite.
262
+ */
263
+ const isStub: (tool: ResourceToolPolicy) => boolean = (
264
+ tool: ResourceToolPolicy,
265
+ ): boolean => {
266
+ return tool
267
+ .evaluateArgv([tool.programs[0] || ""])
268
+ .reason.includes("not implemented yet");
269
+ };
270
+
271
+ test.each(
272
+ ALL_AI_RESOURCE_TYPES.map((type: AiResourceType) => {
273
+ return [type];
274
+ }),
275
+ )(
276
+ "%s: every Test connection command is Denied while its policy is a stub",
277
+ (type: AiResourceType) => {
278
+ const tool: ResourceToolPolicy =
279
+ ResourceCommandPolicy.getToolPolicy(type);
280
+
281
+ if (!isStub(tool)) {
282
+ return;
283
+ }
284
+
285
+ for (const command of AI_RESOURCE_TYPE_INFO[type].testCommands) {
286
+ const result: ResourceCommandPolicyResult = evaluate(command, type);
287
+
288
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
289
+ expect(result.reason).toContain("not implemented yet");
290
+ expect(
291
+ ResourceCommandPolicy.isReadOnly({ resourceType: type, command }),
292
+ ).toBe(false);
293
+ }
294
+ },
295
+ );
296
+
297
+ test.each(
298
+ stubs.map((tool: ResourceToolPolicy): [string, ResourceToolPolicy] => {
299
+ return [tool.name, tool];
300
+ }),
301
+ )(
302
+ "the %s stub is total and denies garbage without throwing",
303
+ (_name: string, tool: ResourceToolPolicy) => {
304
+ if (!isStub(tool)) {
305
+ return;
306
+ }
307
+
308
+ for (const argv of [
309
+ [],
310
+ [tool.programs[0] || ""],
311
+ [tool.programs[0] || "", "anything", "at", "all"],
312
+ null,
313
+ undefined,
314
+ [1, {}, null],
315
+ ]) {
316
+ const result: ResourceCommandPolicyResult = tool.evaluateArgv(
317
+ argv as unknown as Array<string>,
318
+ );
319
+
320
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
321
+ expect(result.targets).toEqual([]);
322
+ }
323
+
324
+ expect(tool.readCommandGuide).toContain("Unavailable");
325
+ expect(tool.writeCommandGuide).toContain("not implemented yet");
326
+ },
327
+ );
328
+ });
329
+
330
+ describe("evaluateArgv / evaluateCommand fail closed before the tool", () => {
331
+ let toolSpy: jest.SpyInstance;
332
+
333
+ beforeEach(() => {
334
+ useFakeTools();
335
+ toolSpy = jest.spyOn(FAKE_DOCKER, "evaluateArgv");
336
+ });
337
+
338
+ test.each([
339
+ ["pvesh get /version", '"docker"'],
340
+ ["govc about", "Docker AI agent"],
341
+ ["kubectl get pods", "Docker host"],
342
+ ["systemctl restart docker", '"docker"'],
343
+ ["Docker ps", '"docker"'],
344
+ ])(
345
+ "%p is Denied for a Docker host: not one of its programs",
346
+ (command: string, mentions: string) => {
347
+ const result: ResourceCommandPolicyResult = evaluate(command);
348
+
349
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
350
+ expect(result.reason).toContain(mentions);
351
+ expect(result.reason).toContain(
352
+ "is not a program the Docker AI agent runs",
353
+ );
354
+ expect(toolSpy).not.toHaveBeenCalled();
355
+ },
356
+ );
357
+
358
+ test("a program written with a path is told to drop the path", () => {
359
+ const result: ResourceCommandPolicyResult = evaluate("/usr/bin/docker ps");
360
+
361
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
362
+ expect(result.reason).toContain("without a path");
363
+ expect(toolSpy).not.toHaveBeenCalled();
364
+ });
365
+
366
+ test("a host program is Denied for a Docker host and docker is Denied for a Host", () => {
367
+ expect(evaluate("uptime").tier).toBe(ResourceCommandTier.Denied);
368
+ expect(evaluate("docker ps", AiResourceType.Host).reason).toContain(
369
+ "Host AI agent",
370
+ );
371
+ });
372
+
373
+ test.each([
374
+ [null],
375
+ [undefined],
376
+ ["docker ps"],
377
+ [[]],
378
+ [["docker", 7]],
379
+ [["docker", null]],
380
+ [
381
+ [
382
+ {
383
+ toString: (): string => {
384
+ return "docker";
385
+ },
386
+ },
387
+ "ps",
388
+ ],
389
+ ],
390
+ ])("argv %p is Denied", (argv: unknown) => {
391
+ const result: ResourceCommandPolicyResult =
392
+ ResourceCommandPolicy.evaluateArgv({
393
+ resourceType: AiResourceType.DockerHost,
394
+ argv: argv as Array<string>,
395
+ });
396
+
397
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
398
+ expect(toolSpy).not.toHaveBeenCalled();
399
+ });
400
+
401
+ test("more than 64 words is Denied", () => {
402
+ const argv: Array<string> = [
403
+ "docker",
404
+ ...Array.from({ length: 64 }, (): string => {
405
+ return "ps";
406
+ }),
407
+ ];
408
+
409
+ expect(
410
+ ResourceCommandPolicy.evaluateArgv({
411
+ resourceType: AiResourceType.DockerHost,
412
+ argv,
413
+ }).reason,
414
+ ).toContain("at most 64 words");
415
+ expect(toolSpy).not.toHaveBeenCalled();
416
+ });
417
+
418
+ test("more than 2000 characters of words is Denied", () => {
419
+ expect(
420
+ ResourceCommandPolicy.evaluateArgv({
421
+ resourceType: AiResourceType.DockerHost,
422
+ argv: ["docker", "logs", "x".repeat(2000)],
423
+ }).reason,
424
+ ).toContain("2000-character limit");
425
+ expect(toolSpy).not.toHaveBeenCalled();
426
+ });
427
+
428
+ test.each([["web\nrm"], ["web\r"], ["web\0"]])(
429
+ "a word holding %p is Denied",
430
+ (word: string) => {
431
+ expect(
432
+ ResourceCommandPolicy.evaluateArgv({
433
+ resourceType: AiResourceType.DockerHost,
434
+ argv: ["docker", "restart", word],
435
+ }).reason,
436
+ ).toContain("single line");
437
+ expect(toolSpy).not.toHaveBeenCalled();
438
+ },
439
+ );
440
+
441
+ test.each([[["sudo", "docker", "ps"]], [["/usr/bin/sudo", "docker", "ps"]]])(
442
+ "argv %p is Denied as a privilege switch",
443
+ (argv: Array<string>) => {
444
+ expect(
445
+ ResourceCommandPolicy.evaluateArgv({
446
+ resourceType: AiResourceType.DockerHost,
447
+ argv,
448
+ }).reason,
449
+ ).toContain("own permissions");
450
+ },
451
+ );
452
+
453
+ test("shell syntax in a command string is Denied with the rule", () => {
454
+ const result: ResourceCommandPolicyResult = evaluate(
455
+ "docker ps | grep web",
456
+ );
457
+
458
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
459
+ expect(result.reason).toContain(
460
+ "pipes and redirects are not supported; run one command",
461
+ );
462
+ expect(result.displayCommand).toBe("docker ps | grep web");
463
+ expect(result.program).toBe("");
464
+ expect(result.args).toEqual([]);
465
+ expect(toolSpy).not.toHaveBeenCalled();
466
+ });
467
+
468
+ test("an unparseable command's display is trimmed and capped at 200 characters", () => {
469
+ const long: string = ` docker logs ${"x".repeat(300)} | `;
470
+
471
+ expect(evaluate(long).displayCommand).toBe(long.trim().slice(0, 200));
472
+ });
473
+
474
+ test("a non-string command is Denied", () => {
475
+ expect(
476
+ ResourceCommandPolicy.evaluateCommand({
477
+ resourceType: AiResourceType.DockerHost,
478
+ command: null as unknown as string,
479
+ }).tier,
480
+ ).toBe(ResourceCommandTier.Denied);
481
+ });
482
+
483
+ test("a valid command reaches the tool with a copy of the argv", () => {
484
+ const result: ResourceCommandPolicyResult = evaluate("docker ps -a");
485
+
486
+ expect(result.tier).toBe(ResourceCommandTier.Read);
487
+ expect(result.program).toBe("docker");
488
+ expect(result.args).toEqual(["ps", "-a"]);
489
+ expect(toolSpy).toHaveBeenCalledWith(["docker", "ps", "-a"]);
490
+ });
491
+ });
492
+
493
+ describe("evaluateArgv distrusts the tool's answer", () => {
494
+ function withTool(
495
+ evaluateArgv: (argv: Array<string>) => unknown,
496
+ ): ResourceCommandPolicyResult {
497
+ jest.spyOn(ResourceCommandPolicy, "getToolPolicy").mockReturnValue({
498
+ ...FAKE_DOCKER,
499
+ evaluateArgv: evaluateArgv as ResourceToolPolicy["evaluateArgv"],
500
+ });
501
+
502
+ return ResourceCommandPolicy.evaluateArgv({
503
+ resourceType: AiResourceType.DockerHost,
504
+ argv: ["docker", "restart", "web"],
505
+ });
506
+ }
507
+
508
+ test("a tool that throws is Denied", () => {
509
+ const result: ResourceCommandPolicyResult = withTool((): never => {
510
+ throw new Error("boom");
511
+ });
512
+
513
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
514
+ expect(result.reason).toContain("could not evaluate");
515
+ });
516
+
517
+ test.each([[null], [undefined], ["SafeWrite"], [42]])(
518
+ "a tool that answers %p is Denied",
519
+ (answer: unknown) => {
520
+ expect(
521
+ withTool((): unknown => {
522
+ return answer;
523
+ }).tier,
524
+ ).toBe(ResourceCommandTier.Denied);
525
+ },
526
+ );
527
+
528
+ test.each([["safewrite"], ["Allowed"], [""], [undefined], [1]])(
529
+ "a tool that answers the tier %p is Denied",
530
+ (tier: unknown) => {
531
+ const result: ResourceCommandPolicyResult = withTool((): unknown => {
532
+ return { tier, reason: "fine", targets: ["web"] };
533
+ });
534
+
535
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
536
+ expect(result.reason).toContain("unknown tier");
537
+ },
538
+ );
539
+
540
+ test("the program is always argv[0] and missing fields are filled in", () => {
541
+ const result: ResourceCommandPolicyResult = withTool((): unknown => {
542
+ return {
543
+ tier: ResourceCommandTier.SafeWrite,
544
+ program: "sh",
545
+ targets: ["web", 7, null],
546
+ requiresHuman: "yes",
547
+ };
548
+ });
549
+
550
+ expect(result).toEqual({
551
+ tier: ResourceCommandTier.SafeWrite,
552
+ reason: "no reason was given",
553
+ program: "docker",
554
+ args: ["restart", "web"],
555
+ verb: "",
556
+ displayCommand: "docker restart web",
557
+ targets: ["web"],
558
+ });
559
+ });
560
+
561
+ test("requiresHuman is kept only as a literal true", () => {
562
+ expect(
563
+ withTool((): unknown => {
564
+ return {
565
+ tier: ResourceCommandTier.RiskyWrite,
566
+ reason: "r",
567
+ requiresHuman: true,
568
+ };
569
+ }).requiresHuman,
570
+ ).toBe(true);
571
+ });
572
+
573
+ test("a tool that mutates the argv it was given cannot change the caller's", () => {
574
+ const argv: Array<string> = ["docker", "restart", "web"];
575
+
576
+ jest.spyOn(ResourceCommandPolicy, "getToolPolicy").mockReturnValue({
577
+ ...FAKE_DOCKER,
578
+ evaluateArgv: (given: Array<string>): ResourceCommandPolicyResult => {
579
+ given[2] = "oneuptime-ai-agent";
580
+ return FAKE_DOCKER.evaluateArgv(given);
581
+ },
582
+ });
583
+
584
+ ResourceCommandPolicy.evaluateArgv({
585
+ resourceType: AiResourceType.DockerHost,
586
+ argv,
587
+ });
588
+
589
+ expect(argv).toEqual(["docker", "restart", "web"]);
590
+ });
591
+ });
592
+
593
+ describe("evaluateForAutoExecution — the ladder through the dispatcher", () => {
594
+ beforeEach(() => {
595
+ useFakeTools();
596
+ });
597
+
598
+ function verdictFor(data: {
599
+ command: string;
600
+ allowlistPatterns?: Array<string>;
601
+ bypassApproval?: boolean;
602
+ }): ResourceAutoExecutionVerdict {
603
+ return ResourceCommandPolicy.evaluateForAutoExecution({
604
+ resourceType: AiResourceType.DockerHost,
605
+ command: data.command,
606
+ allowlistPatterns: data.allowlistPatterns || [],
607
+ bypassApproval: data.bypassApproval === true,
608
+ });
609
+ }
610
+
611
+ test("Denied is Denied, bypass and allowlist included", () => {
612
+ const verdict: ResourceAutoExecutionVerdict = verdictFor({
613
+ command: "docker exec web sh",
614
+ allowlistPatterns: ["docker exec * sh"],
615
+ bypassApproval: true,
616
+ });
617
+
618
+ expect(verdict.verdict).toBe(AiRemediationCommandPolicyVerdict.Denied);
619
+ expect(verdict.tier).toBe(ResourceCommandTier.Denied);
620
+ });
621
+
622
+ test("a read auto-approves", () => {
623
+ expect(verdictFor({ command: "docker ps" }).verdict).toBe(
624
+ AiRemediationCommandPolicyVerdict.AutoApproved,
625
+ );
626
+ });
627
+
628
+ test("a safe write auto-approves", () => {
629
+ const verdict: ResourceAutoExecutionVerdict = verdictFor({
630
+ command: "docker restart web",
631
+ });
632
+
633
+ expect(verdict.verdict).toBe(
634
+ AiRemediationCommandPolicyVerdict.AutoApproved,
635
+ );
636
+ expect(verdict.tier).toBe(ResourceCommandTier.SafeWrite);
637
+ });
638
+
639
+ test("a risky write asks, unless bypassed or allowlisted", () => {
640
+ expect(verdictFor({ command: "docker stop web" }).verdict).toBe(
641
+ AiRemediationCommandPolicyVerdict.RequiresApproval,
642
+ );
643
+ expect(
644
+ verdictFor({ command: "docker stop web", bypassApproval: true }).verdict,
645
+ ).toBe(AiRemediationCommandPolicyVerdict.AutoApproved);
646
+ expect(
647
+ verdictFor({
648
+ command: "docker stop web",
649
+ allowlistPatterns: ["docker stop *"],
650
+ }).reason,
651
+ ).toBe("Matched the resource's command allowlist.");
652
+ expect(
653
+ verdictFor({
654
+ command: "docker stop api",
655
+ allowlistPatterns: ["docker stop web"],
656
+ }).verdict,
657
+ ).toBe(AiRemediationCommandPolicyVerdict.RequiresApproval);
658
+ });
659
+
660
+ test("requiresHuman asks whatever the operator set", () => {
661
+ const verdict: ResourceAutoExecutionVerdict = verdictFor({
662
+ command: "docker kill web",
663
+ allowlistPatterns: ["docker kill *"],
664
+ bypassApproval: true,
665
+ });
666
+
667
+ expect(verdict.verdict).toBe(
668
+ AiRemediationCommandPolicyVerdict.RequiresApproval,
669
+ );
670
+ expect(verdict.requiresHuman).toBe(true);
671
+ });
672
+
673
+ test("an unparseable command is Denied", () => {
674
+ expect(
675
+ verdictFor({ command: "docker stop web; docker rm web" }).verdict,
676
+ ).toBe(AiRemediationCommandPolicyVerdict.Denied);
677
+ });
678
+ });
679
+
680
+ describe("allowlist", () => {
681
+ beforeEach(() => {
682
+ useFakeTools();
683
+ });
684
+
685
+ function problem(
686
+ pattern: unknown,
687
+ resourceType: AiResourceType = AiResourceType.DockerHost,
688
+ ): string | null {
689
+ return ResourceCommandPolicy.describeAllowlistPatternProblem({
690
+ resourceType,
691
+ pattern,
692
+ });
693
+ }
694
+
695
+ test("pins the bounds", () => {
696
+ expect(RESOURCE_ALLOWLIST_MAX_PATTERNS).toBe(50);
697
+ expect(RESOURCE_ALLOWLIST_MAX_PATTERN_LENGTH).toBe(500);
698
+ });
699
+
700
+ describe("describeAllowlistPatternProblem", () => {
701
+ test.each([
702
+ ["docker stop *"],
703
+ ["docker stop web"],
704
+ ["docker restart *"],
705
+ ["docker update --memory * web"],
706
+ ["docker update --memory 512 *"],
707
+ ["docker system prune"],
708
+ [" docker stop * "],
709
+ ])("%p is valid", (pattern: string) => {
710
+ expect(problem(pattern)).toBeNull();
711
+ });
712
+
713
+ test("a numeric-only target can be allowlisted with *", () => {
714
+ expect(problem("ceph osd out *", AiResourceType.CephCluster)).toBeNull();
715
+ });
716
+
717
+ test.each([[""], [" "], [null], [undefined], [42], [["docker stop *"]]])(
718
+ "%p is blank",
719
+ (pattern: unknown) => {
720
+ expect(problem(pattern)).toBe("An allowlist entry cannot be blank.");
721
+ },
722
+ );
723
+
724
+ test("longer than 500 characters", () => {
725
+ expect(problem(`docker stop ${"x".repeat(500)}`)).toBe(
726
+ "An allowlist entry can be at most 500 characters long.",
727
+ );
728
+ });
729
+
730
+ test("does not parse", () => {
731
+ expect(problem("docker stop * | xargs")).toContain(
732
+ "cannot be read as one command",
733
+ );
734
+ expect(problem("docker stop 'web")).toContain("Unbalanced quotes");
735
+ });
736
+
737
+ test.each([
738
+ ["stop *"],
739
+ ["pvesh create /x"],
740
+ ["* stop web"],
741
+ ["sudo docker stop *"],
742
+ ])("%p does not start with the type's program", (pattern: string) => {
743
+ const text: string | null = problem(pattern);
744
+
745
+ expect(text).not.toBeNull();
746
+ expect(text).toMatch(
747
+ /does not start with a program the Docker AI agent runs|own permissions/,
748
+ );
749
+ });
750
+
751
+ test.each([["docker"], ["docker stop"], ["docker *"]])(
752
+ "%p has fewer than two words after the program",
753
+ (pattern: string) => {
754
+ expect(problem(pattern)).toContain(
755
+ 'has fewer than two words after "docker"',
756
+ );
757
+ },
758
+ );
759
+
760
+ test("a * where the command goes", () => {
761
+ expect(problem("docker * web")).toContain(
762
+ "has a * where the command goes",
763
+ );
764
+ });
765
+
766
+ test("a read pre-approves nothing", () => {
767
+ expect(problem("docker ps -a")).toContain("is a read-only command");
768
+ expect(problem("docker ps *")).toContain("is a read-only command");
769
+ });
770
+
771
+ test("a denied command can never match", () => {
772
+ expect(problem("docker exec * sh")).toContain(
773
+ "can never match a command that runs",
774
+ );
775
+ expect(problem("docker exec * sh")).toContain(
776
+ "docker exec is not allowed",
777
+ );
778
+ });
779
+
780
+ test("an entry for the wrong resource type", () => {
781
+ expect(problem("docker stop *", AiResourceType.CephCluster)).toContain(
782
+ "Ceph AI agent",
783
+ );
784
+ });
785
+
786
+ test("an unknown resource type", () => {
787
+ expect(problem("docker stop *", "Nope" as AiResourceType)).toContain(
788
+ "no command policy",
789
+ );
790
+ });
791
+ });
792
+
793
+ describe("describeAllowlistProblems", () => {
794
+ test("a valid list", () => {
795
+ expect(
796
+ ResourceCommandPolicy.describeAllowlistProblems({
797
+ resourceType: AiResourceType.DockerHost,
798
+ patterns: ["docker stop *", "docker restart web"],
799
+ }),
800
+ ).toBeNull();
801
+ expect(
802
+ ResourceCommandPolicy.describeAllowlistProblems({
803
+ resourceType: AiResourceType.DockerHost,
804
+ patterns: [],
805
+ }),
806
+ ).toBeNull();
807
+ });
808
+
809
+ test.each([[null], ["docker stop *"], [{ 0: "docker stop *" }]])(
810
+ "%p is not a list",
811
+ (patterns: unknown) => {
812
+ expect(
813
+ ResourceCommandPolicy.describeAllowlistProblems({
814
+ resourceType: AiResourceType.DockerHost,
815
+ patterns,
816
+ }),
817
+ ).toBe("The allowlist must be a list of commands.");
818
+ },
819
+ );
820
+
821
+ test("more than 50 entries", () => {
822
+ const patterns: Array<string> = Array.from(
823
+ { length: 51 },
824
+ (_: unknown, i: number): string => {
825
+ return `docker stop web-${i}`;
826
+ },
827
+ );
828
+
829
+ expect(
830
+ ResourceCommandPolicy.describeAllowlistProblems({
831
+ resourceType: AiResourceType.DockerHost,
832
+ patterns,
833
+ }),
834
+ ).toContain("at most 50 entries");
835
+ expect(
836
+ ResourceCommandPolicy.describeAllowlistProblems({
837
+ resourceType: AiResourceType.DockerHost,
838
+ patterns: patterns.slice(0, 50),
839
+ }),
840
+ ).toBeNull();
841
+ });
842
+
843
+ test("names the first bad entry", () => {
844
+ expect(
845
+ ResourceCommandPolicy.describeAllowlistProblems({
846
+ resourceType: AiResourceType.DockerHost,
847
+ patterns: ["docker stop *", "docker ps -a", "docker exec * sh"],
848
+ }),
849
+ ).toContain('"docker ps -a" is a read-only command');
850
+ });
851
+ });
852
+
853
+ describe("matchesAllowlist", () => {
854
+ function matches(command: string, patterns: Array<string>): boolean {
855
+ return ResourceCommandPolicy.matchesAllowlist({
856
+ resourceType: AiResourceType.DockerHost,
857
+ command,
858
+ patterns,
859
+ });
860
+ }
861
+
862
+ test("token by token, a whole-word * for one word", () => {
863
+ expect(matches("docker stop web", ["docker stop *"])).toBe(true);
864
+ expect(matches("docker stop web", ["docker stop web"])).toBe(true);
865
+ expect(matches("docker stop web", ["docker stop api"])).toBe(false);
866
+ expect(matches("docker stop web-1", ["docker stop web-*"])).toBe(false);
867
+ expect(
868
+ matches("docker update --memory 512 web", [
869
+ "docker update --memory * web",
870
+ ]),
871
+ ).toBe(true);
872
+ });
873
+
874
+ test("quoting in the command and the pattern is read the same way", () => {
875
+ expect(matches("docker stop 'web'", ['docker stop "web"'])).toBe(true);
876
+ });
877
+
878
+ test("an entry describeAllowlistPatternProblem refuses is skipped", () => {
879
+ // "docker ps *" would match word for word, but it is a read entry.
880
+ expect(matches("docker ps -a", ["docker ps *"])).toBe(false);
881
+ expect(matches("docker exec web sh", ["docker exec * sh"])).toBe(false);
882
+ });
883
+
884
+ test("only the first 50 entries are read", () => {
885
+ const filler: Array<string> = Array.from(
886
+ { length: 50 },
887
+ (_: unknown, i: number): string => {
888
+ return `docker stop filler-${i}`;
889
+ },
890
+ );
891
+
892
+ expect(matches("docker stop web", [...filler, "docker stop web"])).toBe(
893
+ false,
894
+ );
895
+ expect(
896
+ matches("docker stop web", [...filler.slice(1), "docker stop web"]),
897
+ ).toBe(true);
898
+ });
899
+
900
+ test("garbage never matches", () => {
901
+ expect(matches("docker stop web | x", ["docker stop *"])).toBe(false);
902
+ expect(matches("", ["docker stop *"])).toBe(false);
903
+ expect(
904
+ ResourceCommandPolicy.matchesAllowlist({
905
+ resourceType: AiResourceType.DockerHost,
906
+ command: "docker stop web",
907
+ patterns: null as unknown as Array<string>,
908
+ }),
909
+ ).toBe(false);
910
+ expect(
911
+ matches("docker stop web", [
912
+ 7 as unknown as string,
913
+ null as unknown as string,
914
+ ]),
915
+ ).toBe(false);
916
+ });
917
+ });
918
+
919
+ describe("isBroadAllowlistPattern", () => {
920
+ function broad(
921
+ pattern: unknown,
922
+ resourceType: AiResourceType = AiResourceType.DockerHost,
923
+ ): boolean {
924
+ return ResourceCommandPolicy.isBroadAllowlistPattern({
925
+ resourceType,
926
+ pattern,
927
+ });
928
+ }
929
+
930
+ test("a * that stands for the target is broad", () => {
931
+ expect(broad("docker stop *")).toBe(true);
932
+ expect(broad("docker update --memory 512 *")).toBe(true);
933
+ expect(broad("ceph osd out *", AiResourceType.CephCluster)).toBe(true);
934
+ });
935
+
936
+ test("a * that stands for a value, not a target, is not broad", () => {
937
+ expect(broad("docker update --memory * web")).toBe(false);
938
+ });
939
+
940
+ test("an entry without a * is not broad", () => {
941
+ expect(broad("docker stop web")).toBe(false);
942
+ expect(broad("docker system prune")).toBe(false);
943
+ });
944
+
945
+ test("an invalid entry is never broad", () => {
946
+ expect(broad("docker ps *")).toBe(false);
947
+ expect(broad("docker exec * sh")).toBe(false);
948
+ expect(broad("")).toBe(false);
949
+ expect(broad(null)).toBe(false);
950
+ expect(broad("docker stop *", AiResourceType.CephCluster)).toBe(false);
951
+ });
952
+ });
953
+ });
954
+
955
+ describe("getWriteScopeRefusal", () => {
956
+ function scope(
957
+ result: Partial<ResourceCommandPolicyResult>,
958
+ posture: {
959
+ allowWrites?: boolean;
960
+ writeTargets?: Array<string>;
961
+ protectedTargets?: Array<string>;
962
+ resourceType?: AiResourceType;
963
+ } = {},
964
+ ): string | null {
965
+ return ResourceCommandPolicy.getWriteScopeRefusal({
966
+ result: {
967
+ tier: ResourceCommandTier.SafeWrite,
968
+ reason: "restarts one container",
969
+ program: "docker",
970
+ args: ["restart", "web"],
971
+ verb: "restart",
972
+ displayCommand: "docker restart web",
973
+ targets: ["web"],
974
+ ...result,
975
+ },
976
+ allowWrites: posture.allowWrites ?? true,
977
+ writeTargets: posture.writeTargets ?? [],
978
+ protectedTargets: posture.protectedTargets ?? [],
979
+ resourceType: posture.resourceType ?? AiResourceType.DockerHost,
980
+ });
981
+ }
982
+
983
+ test("a read is never refused, even by a read-only agent", () => {
984
+ expect(
985
+ scope(
986
+ { tier: ResourceCommandTier.Read, targets: [] },
987
+ { allowWrites: false, writeTargets: ["nothing"] },
988
+ ),
989
+ ).toBeNull();
990
+ });
991
+
992
+ test("a write the scope allows", () => {
993
+ expect(scope({})).toBeNull();
994
+ expect(scope({ tier: ResourceCommandTier.RiskyWrite })).toBeNull();
995
+ });
996
+
997
+ test("a Denied command is refused", () => {
998
+ expect(
999
+ scope({ tier: ResourceCommandTier.Denied, reason: "exec is denied" }),
1000
+ ).toContain("exec is denied");
1001
+ });
1002
+
1003
+ test("an unknown tier is refused", () => {
1004
+ expect(scope({ tier: "Mystery" as ResourceCommandTier })).not.toBeNull();
1005
+ });
1006
+
1007
+ test("a missing result is refused", () => {
1008
+ expect(
1009
+ ResourceCommandPolicy.getWriteScopeRefusal({
1010
+ result: null as unknown as ResourceCommandPolicyResult,
1011
+ allowWrites: true,
1012
+ writeTargets: [],
1013
+ protectedTargets: [],
1014
+ resourceType: AiResourceType.DockerHost,
1015
+ }),
1016
+ ).not.toBeNull();
1017
+ });
1018
+
1019
+ test.each(
1020
+ ALL_AI_RESOURCE_TYPES.map((type: AiResourceType) => {
1021
+ return [type];
1022
+ }),
1023
+ )(
1024
+ "a read-only %s agent refuses every write and names ONEUPTIME_AI_ALLOW_WRITES",
1025
+ (type: AiResourceType) => {
1026
+ const refusal: string | null = scope(
1027
+ {},
1028
+ { allowWrites: false, resourceType: type },
1029
+ );
1030
+
1031
+ expect(refusal).toContain("ONEUPTIME_AI_ALLOW_WRITES=true");
1032
+ expect(refusal).toContain(AI_RESOURCE_TYPE_INFO[type].agentDisplayName);
1033
+ expect(refusal).toContain("read-only");
1034
+ expect(refusal).toContain("docker restart web");
1035
+ },
1036
+ );
1037
+
1038
+ test("only a literal true allows writes", () => {
1039
+ expect(
1040
+ ResourceCommandPolicy.getWriteScopeRefusal({
1041
+ result: {
1042
+ tier: ResourceCommandTier.SafeWrite,
1043
+ reason: "r",
1044
+ program: "docker",
1045
+ args: [],
1046
+ verb: "",
1047
+ displayCommand: "docker restart web",
1048
+ targets: ["web"],
1049
+ },
1050
+ allowWrites: "true" as unknown as boolean,
1051
+ writeTargets: [],
1052
+ protectedTargets: [],
1053
+ resourceType: AiResourceType.DockerHost,
1054
+ }),
1055
+ ).toContain("ONEUPTIME_AI_ALLOW_WRITES");
1056
+ });
1057
+
1058
+ describe("protected targets", () => {
1059
+ test.each([
1060
+ ["oneuptime-ai-agent", "oneuptime-ai-agent"],
1061
+ ["ONEUPTIME-AI-AGENT", "oneuptime-ai-agent"],
1062
+ ["/oneuptime-ai-agent", "oneuptime-ai-agent"],
1063
+ ["oneuptime-ai-agent", "/oneuptime-ai-agent"],
1064
+ ["oneuptime-collector", "oneuptime-*"],
1065
+ ["3f2a", "3f2a9c1b7d4e5f60"],
1066
+ ["3f2a9c1b7d4e5f60", "3f2a9c1b7d4e5f60"],
1067
+ ["oneuptime-ai-agent.service", "oneuptime-ai-agent.service"],
1068
+ ])(
1069
+ "%p names the protected %p",
1070
+ (target: string, protectedTarget: string) => {
1071
+ const refusal: string | null = scope(
1072
+ { targets: [target] },
1073
+ { protectedTargets: [protectedTarget] },
1074
+ );
1075
+
1076
+ expect(refusal).toContain("protects");
1077
+ expect(refusal).toContain(target);
1078
+ },
1079
+ );
1080
+
1081
+ test.each([
1082
+ ["web", "oneuptime-ai-agent"],
1083
+ ["3f2b", "3f2a9c1b7d4e5f60"],
1084
+ ["abc", "abc-agent"],
1085
+ // A short protected name is not an id, so a hex target is not its prefix.
1086
+ ["ab", "abcdef"],
1087
+ ["oneuptime", "oneuptime-ai-agent"],
1088
+ ])(
1089
+ "%p is not the protected %p",
1090
+ (target: string, protectedTarget: string) => {
1091
+ expect(
1092
+ scope({ targets: [target] }, { protectedTargets: [protectedTarget] }),
1093
+ ).toBeNull();
1094
+ },
1095
+ );
1096
+
1097
+ test("protection beats the write targets allowlist", () => {
1098
+ expect(
1099
+ scope(
1100
+ { targets: ["oneuptime-ai-agent"] },
1101
+ {
1102
+ writeTargets: ["*"],
1103
+ protectedTargets: ["oneuptime-ai-agent"],
1104
+ },
1105
+ ),
1106
+ ).toContain("protects");
1107
+ });
1108
+
1109
+ test("every target is checked, not just the first", () => {
1110
+ expect(
1111
+ scope(
1112
+ { targets: ["web", "oneuptime-ai-agent"] },
1113
+ { protectedTargets: ["oneuptime-ai-agent"] },
1114
+ ),
1115
+ ).toContain("oneuptime-ai-agent");
1116
+ });
1117
+
1118
+ test("blank and non-string protected entries are ignored", () => {
1119
+ expect(
1120
+ scope(
1121
+ { targets: ["web"] },
1122
+ {
1123
+ protectedTargets: ["", " ", 7 as unknown as string],
1124
+ },
1125
+ ),
1126
+ ).toBeNull();
1127
+ });
1128
+ });
1129
+
1130
+ describe("ONEUPTIME_AI_WRITE_TARGETS", () => {
1131
+ test("targets inside the globs are allowed", () => {
1132
+ expect(
1133
+ scope({ targets: ["web-1"] }, { writeTargets: ["web-*", "api"] }),
1134
+ ).toBeNull();
1135
+ expect(
1136
+ scope(
1137
+ { targets: ["api", "web-2"] },
1138
+ { writeTargets: ["web-*", "api"] },
1139
+ ),
1140
+ ).toBeNull();
1141
+ });
1142
+
1143
+ test("a target outside the globs is refused and names the variable", () => {
1144
+ const refusal: string | null = scope(
1145
+ { targets: ["web-1", "db"] },
1146
+ { writeTargets: ["web-*"] },
1147
+ );
1148
+
1149
+ expect(refusal).toContain("db");
1150
+ expect(refusal).toContain("ONEUPTIME_AI_WRITE_TARGETS=web-*");
1151
+ expect(refusal).toContain("Docker AI agent");
1152
+ });
1153
+
1154
+ test("the globs are case-sensitive (narrower is safer)", () => {
1155
+ expect(
1156
+ scope({ targets: ["Web-1"] }, { writeTargets: ["web-*"] }),
1157
+ ).not.toBeNull();
1158
+ });
1159
+
1160
+ test("a write that names no target is refused when the targets are limited", () => {
1161
+ expect(
1162
+ scope(
1163
+ { targets: [], tier: ResourceCommandTier.RiskyWrite },
1164
+ { writeTargets: ["web-*"] },
1165
+ ),
1166
+ ).toContain("does not name the objects it changes");
1167
+ });
1168
+
1169
+ test("a write that names no target is allowed when the targets are not limited", () => {
1170
+ expect(scope({ targets: [] })).toBeNull();
1171
+ });
1172
+
1173
+ test("blank entries do not count as a limit", () => {
1174
+ expect(
1175
+ scope({ targets: ["anything"] }, { writeTargets: ["", " "] }),
1176
+ ).toBeNull();
1177
+ });
1178
+
1179
+ test("blank targets are ignored rather than matched", () => {
1180
+ expect(
1181
+ scope({ targets: ["", "web-1"] }, { writeTargets: ["web-*"] }),
1182
+ ).toBeNull();
1183
+ });
1184
+ });
1185
+ });
1186
+
1187
+ describe("command guides", () => {
1188
+ test("come from the type's tool policy", () => {
1189
+ useFakeTools();
1190
+
1191
+ expect(
1192
+ ResourceCommandPolicy.getReadCommandGuide(AiResourceType.DockerHost),
1193
+ ).toBe("- docker ps");
1194
+ expect(
1195
+ ResourceCommandPolicy.getWriteCommandGuide(AiResourceType.CephCluster),
1196
+ ).toBe("- ceph osd out ID (risky)");
1197
+ });
1198
+
1199
+ test.each(
1200
+ ALL_AI_RESOURCE_TYPES.map((type: AiResourceType) => {
1201
+ return [type];
1202
+ }),
1203
+ )("%s has non-empty markdown bullet guides", (type: AiResourceType) => {
1204
+ for (const guide of [
1205
+ ResourceCommandPolicy.getReadCommandGuide(type),
1206
+ ResourceCommandPolicy.getWriteCommandGuide(type),
1207
+ ]) {
1208
+ expect(guide.trim().length).toBeGreaterThan(0);
1209
+ expect(guide.trimStart().startsWith("- ")).toBe(true);
1210
+ }
1211
+ });
1212
+
1213
+ test("an unknown type says it is unavailable", () => {
1214
+ expect(
1215
+ ResourceCommandPolicy.getReadCommandGuide("Nope" as AiResourceType),
1216
+ ).toContain("Unavailable");
1217
+ });
1218
+ });
1219
+
1220
+ /*
1221
+ * The allowlist checks with the real tool policies: a `*` is tried with the
1222
+ * generic stand-ins AND the words the tool's own grammar accepts, and an
1223
+ * entry is broad only when the word in place of a `*` changes what the write
1224
+ * touches — never because a stand-in happens to appear inside a target.
1225
+ */
1226
+ describe("allowlist stand-ins with the real tool policies", () => {
1227
+ test.each([
1228
+ [AiResourceType.CephCluster, "ceph orch daemon stop *"],
1229
+ [AiResourceType.CephCluster, "ceph pg repair *"],
1230
+ [AiResourceType.CephCluster, "ceph osd reweight * *"],
1231
+ [AiResourceType.CephCluster, "ceph osd set *"],
1232
+ [AiResourceType.DockerHost, "docker stop *"],
1233
+ [AiResourceType.Host, "systemctl stop *"],
1234
+ [AiResourceType.DatabaseServer, "db terminate-session *"],
1235
+ [AiResourceType.VMwareVCenter, "govc vm.power -off *"],
1236
+ ])("%s: %s is a valid entry", (type: AiResourceType, pattern: string) => {
1237
+ expect(
1238
+ ResourceCommandPolicy.describeAllowlistPatternProblem({
1239
+ resourceType: type,
1240
+ pattern,
1241
+ }),
1242
+ ).toBeNull();
1243
+ });
1244
+
1245
+ test.each([
1246
+ [AiResourceType.CephCluster, "ceph orch daemon stop *"],
1247
+ [AiResourceType.CephCluster, "ceph pg repair *"],
1248
+ [AiResourceType.CephCluster, "ceph osd reweight * *"],
1249
+ [AiResourceType.DockerHost, "docker stop *"],
1250
+ [AiResourceType.Host, "systemctl stop *"],
1251
+ [AiResourceType.DatabaseServer, "db terminate-session *"],
1252
+ [AiResourceType.VMwareVCenter, "govc vm.power -off *"],
1253
+ ])(
1254
+ "%s: %s names any target, so it is broad",
1255
+ (type: AiResourceType, pattern: string) => {
1256
+ expect(
1257
+ ResourceCommandPolicy.isBroadAllowlistPattern({
1258
+ resourceType: type,
1259
+ pattern,
1260
+ }),
1261
+ ).toBe(true);
1262
+ },
1263
+ );
1264
+
1265
+ test.each([
1266
+ // The stand-in "1" appears inside the VMID 101 — that is not a `*` target.
1267
+ [
1268
+ AiResourceType.ProxmoxCluster,
1269
+ "pvesh create /nodes/pve1/qemu/101/status/shutdown --timeout *",
1270
+ ],
1271
+ [AiResourceType.DockerHost, "docker stop -t * web"],
1272
+ // The flag is a value of a cluster-wide change: the target stays "cluster".
1273
+ [AiResourceType.CephCluster, "ceph osd set *"],
1274
+ [AiResourceType.CephCluster, "ceph osd out 3"],
1275
+ [
1276
+ AiResourceType.ProxmoxCluster,
1277
+ "pvesh create /nodes/pve1/qemu/101/status/stop",
1278
+ ],
1279
+ ])(
1280
+ "%s: %s names its target, so it is not broad",
1281
+ (type: AiResourceType, pattern: string) => {
1282
+ expect(
1283
+ ResourceCommandPolicy.isBroadAllowlistPattern({
1284
+ resourceType: type,
1285
+ pattern,
1286
+ }),
1287
+ ).toBe(false);
1288
+ },
1289
+ );
1290
+
1291
+ test("checking a whole allowlist of many-`*` entries stays fast", () => {
1292
+ const patterns: Array<string> = Array.from({ length: 50 }, (): string => {
1293
+ return "ceph osd reweight * *";
1294
+ });
1295
+ const started: number = Date.now();
1296
+
1297
+ expect(
1298
+ ResourceCommandPolicy.describeAllowlistProblems({
1299
+ resourceType: AiResourceType.CephCluster,
1300
+ patterns,
1301
+ }),
1302
+ ).toBeNull();
1303
+ expect(Date.now() - started).toBeLessThan(5000);
1304
+ });
1305
+ });