@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,1259 @@
1
+ import DockerEngineCommandPolicy, {
2
+ DOCKER_ENGINE_PROFILE,
3
+ DOCKER_ENGINE_WRITE_COMMANDS,
4
+ DOCKER_KILL_SIGNALS,
5
+ normalizeDockerKillSignal,
6
+ } from "../../../../Utils/AiRemediation/Resource/DockerEngineCommandPolicy";
7
+ import {
8
+ DOCKER_ENGINE_READ_COMMANDS,
9
+ DOCKER_ENGINE_WRITE_PATHS,
10
+ } from "../../../../Utils/AiRemediation/Resource/DockerCliGrammar";
11
+ import ResourceCommandPolicy from "../../../../Utils/AiRemediation/Resource/ResourceCommandPolicy";
12
+ import {
13
+ ResourceAutoExecutionVerdict,
14
+ ResourceCommandPolicyResult,
15
+ tokenizeResourceCommand,
16
+ } from "../../../../Utils/AiRemediation/Resource/ResourceCommandPolicyCore";
17
+ import AiResourceType, {
18
+ AI_RESOURCE_TYPE_INFO,
19
+ } from "../../../../Types/ResourceAiAgent/AiResourceType";
20
+ import { ResourceCommandTier } from "../../../../Types/ResourceAiAgent/ResourceAiAccess";
21
+ import { AiRemediationCommandPolicyVerdict } from "../../../../Types/AutoRemediation/AiRemediationCommandPolicyVerdict";
22
+ import { describe, expect, test } from "@jest/globals";
23
+
24
+ /*
25
+ * Contract under test — the docker-engine command policy (Docker and Podman
26
+ * hosts), through the dispatcher every caller uses and on its own.
27
+ *
28
+ * - Read: the engine's inspection commands, bounded (logs need --tail N up
29
+ * to 2000 or --since, stats needs --no-stream, events need --since and
30
+ * --until), with --format limited to json/table.
31
+ * - SafeWrite: restart / start / unpause of exactly one container.
32
+ * - RiskyWrite: stop, kill (named signals), pause, update of limits and the
33
+ * restart policy, and a SafeWrite verb naming several containers.
34
+ * - Denied: everything else, every flag before the command (docker's global
35
+ * flags), every flag the command does not model, and every value that is
36
+ * not what docker would read it as — so nothing can hide in a flag.
37
+ * - Targets are the container names as written; reads have none.
38
+ */
39
+
40
+ const DOCKER_TYPES: Array<AiResourceType> = [
41
+ AiResourceType.DockerHost,
42
+ AiResourceType.PodmanHost,
43
+ ];
44
+
45
+ function evaluate(
46
+ command: string,
47
+ resourceType: AiResourceType = AiResourceType.DockerHost,
48
+ ): ResourceCommandPolicyResult {
49
+ return ResourceCommandPolicy.evaluateCommand({ resourceType, command });
50
+ }
51
+
52
+ function evaluateArgv(argv: Array<string>): ResourceCommandPolicyResult {
53
+ return ResourceCommandPolicy.evaluateArgv({
54
+ resourceType: AiResourceType.DockerHost,
55
+ argv,
56
+ });
57
+ }
58
+
59
+ // [command, verb]
60
+ const READS: Array<[string, string]> = [
61
+ ["docker ps", "ps"],
62
+ ["docker ps -a", "ps"],
63
+ ["docker ps -aq", "ps"],
64
+ ["docker ps --all --quiet --no-trunc --size", "ps"],
65
+ ["docker ps -n 5", "ps"],
66
+ ["docker ps -n5", "ps"],
67
+ ["docker ps -n=5", "ps"],
68
+ ["docker ps --last=5", "ps"],
69
+ ["docker ps -l", "ps"],
70
+ ["docker ps -a=false", "ps"],
71
+ ["docker ps --filter status=exited --filter name=web", "ps"],
72
+ ["docker ps -f status=exited", "ps"],
73
+ ["docker ps -af status=exited", "ps"],
74
+ ["docker ps -qf=name=web", "ps"],
75
+ ["docker ps --format json", "ps"],
76
+ ["docker ps --format=table", "ps"],
77
+ ["docker ps --filter 'label=com.example.team=a b'", "ps"],
78
+ ["docker container ls -a", "container ls"],
79
+ ["docker container list", "container ls"],
80
+ ["docker container ps --filter health=unhealthy", "container ls"],
81
+ ["docker inspect --type container web", "inspect"],
82
+ ["docker inspect --type=image nginx:1.27", "inspect"],
83
+ ["docker inspect --type network bridge", "inspect"],
84
+ ["docker inspect --type volume data", "inspect"],
85
+ ["docker inspect -s --type container web api", "inspect"],
86
+ ["docker inspect --type container --format json web", "inspect"],
87
+ ["docker inspect --type container -f json web", "inspect"],
88
+ ["docker inspect web --type container", "inspect"],
89
+ ["docker container inspect web", "container inspect"],
90
+ ["docker container inspect -s web", "container inspect"],
91
+ ["docker container inspect --format=json web db", "container inspect"],
92
+ ["docker container inspect 3f2a9c1b7d4e", "container inspect"],
93
+ ["docker image inspect nginx:1.27", "image inspect"],
94
+ [
95
+ "docker image inspect registry.example.com:5000/team/app@sha256:0123abcd",
96
+ "image inspect",
97
+ ],
98
+ ["docker network inspect bridge", "network inspect"],
99
+ ["docker network inspect -v bridge", "network inspect"],
100
+ ["docker volume inspect data", "volume inspect"],
101
+ ["docker logs --tail 100 web", "logs"],
102
+ ["docker logs -n 100 web", "logs"],
103
+ ["docker logs -n100 web", "logs"],
104
+ ["docker logs --tail=2000 web", "logs"],
105
+ ["docker logs --tail 0 web", "logs"],
106
+ ["docker logs --since 30m web", "logs"],
107
+ ["docker logs --since 1h30m --until 5m web", "logs"],
108
+ ["docker logs --since 24h web", "logs"],
109
+ ["docker logs --since 23h59m web", "logs"],
110
+ [
111
+ "docker logs --tail 500 --since 2026-09-29T10:00:00Z --until 2026-09-29T11:00:00+02:00 web",
112
+ "logs",
113
+ ],
114
+ ["docker logs --tail 500 --since 2026-09-29 web", "logs"],
115
+ ["docker logs --tail 500 --since 1758000000 web", "logs"],
116
+ ["docker logs --tail 500 --since 1758000000.5 web", "logs"],
117
+ ["docker logs --tail 100 --since 100000h web", "logs"],
118
+ ["docker logs -t --details --tail 50 web", "logs"],
119
+ ["docker logs -tn 50 web", "logs"],
120
+ ["docker logs -tn50 web", "logs"],
121
+ ["docker logs web --tail 50", "logs"],
122
+ ["docker logs --tail 50 -- web", "logs"],
123
+ ["docker container logs --tail 50 web", "container logs"],
124
+ ["docker stats --no-stream", "stats"],
125
+ ["docker stats --no-stream web api", "stats"],
126
+ ["docker stats -a --no-stream --no-trunc --format json", "stats"],
127
+ ["docker stats --no-stream=true web", "stats"],
128
+ ["docker stats --no-stream=1 web", "stats"],
129
+ ["docker container stats --no-stream", "container stats"],
130
+ ["docker top web", "top"],
131
+ ["docker container top web", "container top"],
132
+ ["docker events --since 30m --until 0s", "events"],
133
+ [
134
+ "docker events --since 1h --until 30m --filter type=container --format json",
135
+ "events",
136
+ ],
137
+ ["docker system events --since 10m --until 0s", "system events"],
138
+ ["docker info", "info"],
139
+ ["docker info --format json", "info"],
140
+ ["docker system info", "system info"],
141
+ ["docker system info -f json", "system info"],
142
+ ["docker version", "version"],
143
+ ["docker version -f json", "version"],
144
+ ["docker system df", "system df"],
145
+ ["docker system df -v", "system df"],
146
+ ["docker system df -v --format table", "system df"],
147
+ ["docker system df --format json", "system df"],
148
+ ["docker system df -v=false --format json", "system df"],
149
+ ["docker images", "images"],
150
+ ["docker images -a", "images"],
151
+ ["docker images nginx", "images"],
152
+ ["docker images --filter dangling=true -q", "images"],
153
+ ["docker image ls", "image ls"],
154
+ ["docker image list --digests --no-trunc", "image ls"],
155
+ ["docker network ls", "network ls"],
156
+ ["docker network list --no-trunc -q", "network ls"],
157
+ ["docker network ls --filter driver=bridge --format json", "network ls"],
158
+ ["docker volume ls", "volume ls"],
159
+ ["docker volume ls -f dangling=true", "volume ls"],
160
+ ["docker volume list -q --format table", "volume ls"],
161
+ ["docker port web", "port"],
162
+ ["docker port web 80/tcp", "port"],
163
+ ["docker port web 53/udp", "port"],
164
+ ["docker container port web 8080", "container port"],
165
+ ["docker diff web", "diff"],
166
+ ["docker container diff web", "container diff"],
167
+ ];
168
+
169
+ // [command, verb, targets]
170
+ const SAFE_WRITES: Array<[string, string, Array<string>]> = [
171
+ ["docker restart web", "restart", ["web"]],
172
+ ["docker restart -t 10 web", "restart", ["web"]],
173
+ ["docker restart --time 10 web", "restart", ["web"]],
174
+ ["docker restart --timeout=10 web", "restart", ["web"]],
175
+ ["docker restart -t=0 web", "restart", ["web"]],
176
+ ["docker restart web -t5", "restart", ["web"]],
177
+ ["docker restart -- web", "restart", ["web"]],
178
+ ["docker restart 3f2a9c1b7d4e", "restart", ["3f2a9c1b7d4e"]],
179
+ ["docker restart my_app.web-1", "restart", ["my_app.web-1"]],
180
+ ["docker container restart web", "container restart", ["web"]],
181
+ ["docker start web", "start", ["web"]],
182
+ ["docker container start web", "container start", ["web"]],
183
+ ["docker unpause web", "unpause", ["web"]],
184
+ ["docker container unpause web", "container unpause", ["web"]],
185
+ ];
186
+
187
+ // [command, verb, targets]
188
+ const RISKY_WRITES: Array<[string, string, Array<string>]> = [
189
+ ["docker restart web api", "restart", ["web", "api"]],
190
+ ["docker restart web web", "restart", ["web"]],
191
+ ["docker restart -t 5 web api db", "restart", ["web", "api", "db"]],
192
+ ["docker container restart web api", "container restart", ["web", "api"]],
193
+ ["docker start web api", "start", ["web", "api"]],
194
+ ["docker unpause web api", "unpause", ["web", "api"]],
195
+ ["docker stop web", "stop", ["web"]],
196
+ ["docker stop -t 30 web", "stop", ["web"]],
197
+ ["docker stop --time=30 web api", "stop", ["web", "api"]],
198
+ ["docker container stop web", "container stop", ["web"]],
199
+ ["docker kill web", "kill", ["web"]],
200
+ ["docker kill -s TERM web", "kill", ["web"]],
201
+ ["docker kill --signal=SIGHUP web", "kill", ["web"]],
202
+ ["docker kill -s usr1 web", "kill", ["web"]],
203
+ ["docker kill -s sigint web", "kill", ["web"]],
204
+ ["docker kill -sQUIT web", "kill", ["web"]],
205
+ ["docker container kill -s USR2 web api", "container kill", ["web", "api"]],
206
+ ["docker pause web", "pause", ["web"]],
207
+ ["docker container pause web", "container pause", ["web"]],
208
+ ["docker update --memory 512m web", "update", ["web"]],
209
+ ["docker update -m 1g --memory-swap 2g web", "update", ["web"]],
210
+ ["docker update --memory-swap -1 web", "update", ["web"]],
211
+ ["docker update --memory-swap=-1 web", "update", ["web"]],
212
+ ["docker update --memory-reservation 256MiB web", "update", ["web"]],
213
+ ["docker update --cpus 1.5 web", "update", ["web"]],
214
+ ["docker update -c 512 web", "update", ["web"]],
215
+ ["docker update --cpu-shares=1024 web", "update", ["web"]],
216
+ ["docker update --restart unless-stopped web", "update", ["web"]],
217
+ ["docker update --restart on-failure:3 web", "update", ["web"]],
218
+ ["docker update --restart=no web", "update", ["web"]],
219
+ ["docker update --pids-limit 200 web", "update", ["web"]],
220
+ ["docker update --pids-limit -1 web", "update", ["web"]],
221
+ ["docker update web --cpus 2", "update", ["web"]],
222
+ [
223
+ "docker container update --cpus 2 web api",
224
+ "container update",
225
+ ["web", "api"],
226
+ ],
227
+ ];
228
+
229
+ // [command, a phrase the reason must contain]
230
+ const DENIED: Array<[string, string]> = [
231
+ // Families that never run.
232
+ ["docker exec web sh", "runs a program inside a container"],
233
+ ["docker exec -it web sh", "runs a program inside a container"],
234
+ ["docker container exec web ls", "runs a program inside a container"],
235
+ ["docker run nginx", "creates and starts a new container"],
236
+ ["docker run --privileged -v /:/host alpine", "creates and starts"],
237
+ ["docker container run alpine", "creates and starts"],
238
+ ["docker create nginx", "creates a new container"],
239
+ ["docker cp web:/etc/passwd /tmp/x", "copies files"],
240
+ ["docker attach web", "attaches to a container"],
241
+ ["docker rm web", "deletes containers"],
242
+ ["docker rm -f web", "deletes containers"],
243
+ ["docker container rm web", "deletes containers"],
244
+ ["docker container remove web", "deletes containers"],
245
+ ["docker rmi nginx", "deletes images"],
246
+ ["docker image rm nginx", "deletes images"],
247
+ ["docker image remove nginx", "deletes images"],
248
+ ["docker container prune -f", "deletes every stopped container"],
249
+ ["docker image prune -a", "deletes images in bulk"],
250
+ ["docker network prune", "deletes every unused network"],
251
+ ["docker volume prune", "deletes volumes"],
252
+ ["docker system prune -af", "in bulk"],
253
+ ["docker build .", "builds images"],
254
+ ["docker image build .", "builds images"],
255
+ ["docker buildx build .", "builds images"],
256
+ ["docker builder prune", "build cache"],
257
+ ["docker push registry/app:1", "uploads images"],
258
+ ["docker pull nginx", "downloads images"],
259
+ ["docker image pull nginx", "downloads images"],
260
+ ["docker login -u admin registry", "registry credentials"],
261
+ ["docker logout", "registry credentials"],
262
+ ["docker save nginx", "copies images out"],
263
+ ["docker load", "loads images"],
264
+ ["docker export web", "filesystem out"],
265
+ ["docker import x.tar", "creates an image"],
266
+ ["docker commit web img", "new image"],
267
+ ["docker rename web web2", "renames a container"],
268
+ ["docker wait web", "blocks until"],
269
+ ["docker history nginx", "build arguments"],
270
+ ["docker image history nginx", "build arguments"],
271
+ ["docker tag a b", "retags"],
272
+ ["docker search nginx", "queries a registry"],
273
+ ["docker plugin ls", "plugins"],
274
+ ["docker secret ls", "swarm secrets"],
275
+ ["docker config ls", "swarm configs"],
276
+ ["docker swarm init", "swarm membership"],
277
+ ["docker context use other", "switches the engine"],
278
+ ["docker trust inspect nginx", "trust data"],
279
+ ["docker manifest inspect nginx", "manifests"],
280
+ ["docker compose up -d", "not shipped with the agent"],
281
+ ["docker compose ps", "not shipped with the agent"],
282
+ ["docker checkpoint create web cp1", "checkpoints"],
283
+ ["docker network create net1", "container networking"],
284
+ ["docker network rm net1", "container networking"],
285
+ ["docker network connect net1 web", "container networking"],
286
+ ["docker network disconnect net1 web", "container networking"],
287
+ ["docker volume create v1", "volumes"],
288
+ ["docker volume rm v1", "deletes volumes"],
289
+ ["docker system dial-stdio", "raw connection"],
290
+ // Swarm commands belong to the swarm profile.
291
+ ["docker service ls", "swarm command"],
292
+ ["docker service update --force web", "swarm command"],
293
+ ["docker node ls", "swarm command"],
294
+ ["docker node update --availability drain n1", "swarm command"],
295
+ ["docker stack ls", "swarm command"],
296
+ ["docker service", "swarm command"],
297
+ // Not a command at all.
298
+ ["docker foo", "not a docker command"],
299
+ ["docker PS", "docker commands are lowercase"],
300
+ ["docker Restart web", "docker commands are lowercase"],
301
+ ["docker container foo", "not a docker command"],
302
+ ["docker", 'after "docker"'],
303
+ ["docker container", "needs a subcommand"],
304
+ ["docker image", "needs a subcommand"],
305
+ // Global flags, before the command.
306
+ ["docker -H tcp://evil:2375 ps", "global flags"],
307
+ ["docker --host=unix:///var/run/other.sock ps", "global flags"],
308
+ ["docker -c other ps", "global flags"],
309
+ ["docker --context other ps", "global flags"],
310
+ ["docker --config /tmp/x ps", "global flags"],
311
+ ["docker --tls ps", "global flags"],
312
+ ["docker --tlsverify --tlscacert x ps", "global flags"],
313
+ ["docker -D ps", "global flags"],
314
+ ["docker --debug ps", "global flags"],
315
+ ["docker -l debug ps", "global flags"],
316
+ ["docker --log-level debug ps", "global flags"],
317
+ ["docker -v", "global flags"],
318
+ ["docker --version", "global flags"],
319
+ ["docker -- ps", "global flags"],
320
+ ["docker --help", "global flags"],
321
+ // A flag between a group and its subcommand.
322
+ ["docker container -a ls", "comes before the docker container subcommand"],
323
+ ["docker container --help", "comes before the docker container subcommand"],
324
+ // ... and a global flag after the command is just a flag it lacks.
325
+ ["docker ps -H tcp://evil:2375", "global flags"],
326
+ ["docker ps --context other", "global flags"],
327
+ ["docker restart --host=tcp://evil web", "global flags"],
328
+ ["docker logs --tlsverify --tail 5 web", "global flags"],
329
+ // ps
330
+ ["docker ps web", "takes no container names"],
331
+ ["docker ps --format '{{.Names}}'", "Go templates are not allowed"],
332
+ ["docker ps --format 'table {{.Names}}'", "Go templates are not allowed"],
333
+ ["docker ps --format '{{json .}}'", "Go templates are not allowed"],
334
+ ["docker ps --filter", "needs a value"],
335
+ ["docker ps --filter -q", "--filter takes KEY=VALUE"],
336
+ ["docker ps --filter status", "--filter takes KEY=VALUE"],
337
+ ["docker ps -n all", "-n/--last takes a number"],
338
+ ["docker ps -n -1", "-n/--last takes a number"],
339
+ ["docker ps -a -a", "more than once"],
340
+ ["docker ps -aa", "more than once"],
341
+ ["docker ps --all --all=false", "more than once"],
342
+ ["docker ps --all true", 'got "true"'],
343
+ ["docker ps -x", "not one OneUptime AI may use"],
344
+ ["docker ps --no_trunc", "not one OneUptime AI may use"],
345
+ ["docker ps --all=yes", "is a switch"],
346
+ ["docker ps -a=", "not one OneUptime AI may use"],
347
+ ["docker ps ---all", "not valid flag syntax"],
348
+ ["docker ps --=x", "not valid flag syntax"],
349
+ ["docker ps --help", "not one OneUptime AI may use"],
350
+ // inspect
351
+ ["docker inspect web", "needs --type"],
352
+ ["docker inspect --type secret s1", "needs --type"],
353
+ ["docker inspect --type config c1", "needs --type"],
354
+ ["docker inspect --type node n1", "needs --type"],
355
+ ["docker inspect --type Container web", "needs --type"],
356
+ [
357
+ "docker inspect --type container --format '{{json .Config.Env}}' web",
358
+ "Go templates",
359
+ ],
360
+ ["docker inspect --type container -f '{{.Config.Env}}' web", "Go templates"],
361
+ ["docker inspect --type container --format table web", "Go templates"],
362
+ ["docker inspect --type container", "at least one container"],
363
+ ["docker container inspect", "at least one container"],
364
+ [
365
+ "docker container inspect -f '{{range .Config.Env}}{{.}} {{end}}' web",
366
+ "Go templates",
367
+ ],
368
+ ["docker container inspect '{{.Config.Env}}'", "not a container name"],
369
+ ["docker image inspect --platform linux/amd64 nginx", "not one OneUptime AI"],
370
+ ["docker image inspect ' nginx'", "not a image name"],
371
+ ["docker volume inspect -s data", "not one OneUptime AI"],
372
+ // logs
373
+ ["docker logs web", "needs --tail N"],
374
+ ["docker logs -f web", "streams the log"],
375
+ ["docker logs --follow --tail 10 web", "streams the log"],
376
+ ["docker logs --tail 10 -f web", "streams the log"],
377
+ ["docker logs -tf --tail 10 web", "streams the log"],
378
+ ["docker logs --follow=false --tail 10 web", "streams the log"],
379
+ ["docker logs --tail 2001 web", "from 0 to 2000"],
380
+ ["docker logs --tail all web", "from 0 to 2000"],
381
+ ["docker logs --tail -1 web", "from 0 to 2000"],
382
+ ["docker logs --tail +5 web", "from 0 to 2000"],
383
+ ["docker logs --tail 99999999999 web", "from 0 to 2000"],
384
+ ["docker logs --tail '' web", "from 0 to 2000"],
385
+ ["docker logs --since '' web", "--since takes a duration"],
386
+ ["docker logs --since=0 web", "--since takes a duration"],
387
+ ["docker logs --since -f web", "--since takes a duration"],
388
+ ["docker logs --since yesterday web", "--since takes a duration"],
389
+ ["docker logs --since -10m web", "--since takes a duration"],
390
+ ["docker logs --tail 10 --until soon web", "--until takes a duration"],
391
+ ["docker logs --until 5m web", "needs --tail N"],
392
+ // --since alone must bound the slice: a duration of at most 24h
393
+ ["docker logs --since 2001-01-01 web", "is an absolute time"],
394
+ ["docker logs --since 2026-09-29T10:00:00Z web", "is an absolute time"],
395
+ ["docker logs --since 1758000000 web", "is an absolute time"],
396
+ ["docker logs --since 100000h web", "further back than 24h"],
397
+ ["docker logs --since 24h1s web", "further back than 24h"],
398
+ ["docker logs --since 1441m web", "further back than 24h"],
399
+ ["docker logs --tail 10 web api", "exactly one container"],
400
+ ["docker logs --tail 10", "exactly one container"],
401
+ ["docker logs --tail 10 --tail 20 web", "more than once"],
402
+ ["docker logs -n 10 --tail 20 web", "more than once"],
403
+ ["docker logs --tail 10 wеb", "not a container name"],
404
+ // stats
405
+ ["docker stats", "needs --no-stream"],
406
+ ["docker stats web", "needs --no-stream"],
407
+ ["docker stats --no-stream=false", "needs --no-stream"],
408
+ ["docker stats --no-stream --no-stream=false", "more than once"],
409
+ ["docker stats --no-stream --format '{{.Name}}'", "Go templates"],
410
+ ["docker stats --no-stream '--format=table {{.Name}}'", "Go templates"],
411
+ ["docker stats --no-stream=maybe", "is a switch"],
412
+ // top
413
+ ["docker top web aux", "no ps options"],
414
+ ["docker top web -eo pid,args", "no ps options"],
415
+ ["docker top", "exactly one container"],
416
+ ["docker top -x web", "not one OneUptime AI"],
417
+ // events
418
+ ["docker events", "needs both --since and --until"],
419
+ ["docker events --since 10m", "needs both --since and --until"],
420
+ ["docker events --until 0s", "needs both --since and --until"],
421
+ ["docker events --since 10m --until 0s web", "takes no names"],
422
+ [
423
+ "docker events --since 10m --until 0s --format '{{json .}}'",
424
+ "Go templates",
425
+ ],
426
+ ["docker events --since=0 --until 0s", "take a duration"],
427
+ ["docker events --since 10m --until never", "take a duration"],
428
+ // an absolute --until can lie in the future: docker events would stream
429
+ [
430
+ "docker events --since 30m --until 2099-12-31",
431
+ "--until must be a duration",
432
+ ],
433
+ [
434
+ "docker events --since 30m --until 9999999999",
435
+ "--until must be a duration",
436
+ ],
437
+ [
438
+ "docker events --since 30m --until 2026-09-29T11:00:00Z",
439
+ "--until must be a duration",
440
+ ],
441
+ [
442
+ "docker system events --since 1m --until 2099-12-31",
443
+ "--until must be a duration",
444
+ ],
445
+ // info, version, df
446
+ ["docker info --format '{{.ServerVersion}}'", "Go templates"],
447
+ ["docker info extra", "takes no arguments"],
448
+ ["docker version --format '{{.Server.Version}}'", "Go templates"],
449
+ ["docker system df -v --format '{{.Size}}'", "Go templates"],
450
+ // -v with json prints every build-cache record's RUN command line
451
+ ["docker system df -v --format json", "build-cache record's Description"],
452
+ [
453
+ "docker system df --verbose --format=json",
454
+ "build-cache record's Description",
455
+ ],
456
+ ["docker system df --format json -v", "build-cache record's Description"],
457
+ ["docker system df extra", "takes no arguments"],
458
+ // images, networks, volumes
459
+ ["docker images a b", "at most one"],
460
+ ["docker images --tree", "not one OneUptime AI"],
461
+ ["docker network ls extra", "takes no arguments"],
462
+ ["docker volume ls --cluster", "not one OneUptime AI"],
463
+ // port, diff
464
+ ["docker port web http", "is not a PORT"],
465
+ ["docker port web 80 81", "one PORT"],
466
+ ["docker port", "one PORT"],
467
+ ["docker diff", "exactly one container"],
468
+ ["docker diff a b", "exactly one container"],
469
+ // writes with bad shapes
470
+ ["docker restart", "needs the container"],
471
+ ["docker restart -s KILL web", "clean stop"],
472
+ ["docker restart --signal=SIGKILL web", "clean stop"],
473
+ ["docker restart -t abc web", "-t/--timeout takes"],
474
+ ["docker restart -t 601 web", "-t/--timeout takes"],
475
+ ["docker restart -t -1 web", "-t/--timeout takes"],
476
+ ["docker restart -tweb", "-t/--timeout takes"],
477
+ ["docker restart --time 5 --timeout 6 web", "more than once"],
478
+ ["docker restart -t", "needs a value"],
479
+ ["docker restart ''", "not a container name"],
480
+ ["docker restart /web", "not a container name"],
481
+ ["docker restart 'web;rm'", "not a container name"],
482
+ ["docker restart '-web'", "not one OneUptime AI"],
483
+ ["docker restart -- -web", "not a container name"],
484
+ ["docker restart web --privileged", "not one OneUptime AI"],
485
+ ["docker start -a web", "never attaches"],
486
+ ["docker start -ai web", "never attaches"],
487
+ ["docker start --interactive web", "never attaches"],
488
+ ["docker start --checkpoint cp1 web", "checkpoint"],
489
+ ["docker start", "needs the container"],
490
+ ["docker unpause --all", "not one OneUptime AI"],
491
+ ["docker stop -s SIGKILL web", "clean stop"],
492
+ ["docker stop", "needs the container"],
493
+ ["docker kill -s STOP web", "-s/--signal must be one of"],
494
+ ["docker kill -s 9 web", "-s/--signal must be one of"],
495
+ ["docker kill -s SIGSIGTERM web", "-s/--signal must be one of"],
496
+ ["docker kill -s '' web", "-s/--signal must be one of"],
497
+ ["docker kill -s TERM -s KILL web", "more than once"],
498
+ ["docker kill", "needs the container"],
499
+ ["docker pause", "needs the container"],
500
+ ["docker update web", "needs at least one of"],
501
+ ["docker update --memory lots web", "--memory takes"],
502
+ ["docker update --memory -1 web", "--memory takes"],
503
+ ["docker update --cpus two web", "--cpus takes"],
504
+ ["docker update --cpu-shares 999999 web", "--cpu-shares takes"],
505
+ ["docker update --restart sometimes web", "--restart takes"],
506
+ ["docker update --restart on-failure:x web", "--restart takes"],
507
+ ["docker update --pids-limit -2 web", "--pids-limit takes"],
508
+ ["docker update --blkio-weight 100 web", "not one OneUptime AI"],
509
+ ["docker update --cpuset-cpus 0-1 web", "not one OneUptime AI"],
510
+ ["docker update --cpu-quota 50000 web", "not one OneUptime AI"],
511
+ ["docker update --kernel-memory 1g web", "not one OneUptime AI"],
512
+ ["docker update --privileged web", "not one OneUptime AI"],
513
+ ["docker update --memory 1g", "needs the container"],
514
+ ["docker update -m 1g -m 2g web", "more than once"],
515
+ ["docker update -m 1g --memory 2g web", "more than once"],
516
+ ];
517
+
518
+ function describeTargets(targets: Array<string>): string {
519
+ return JSON.stringify(targets);
520
+ }
521
+
522
+ describe("docker-engine: the tool policy object", () => {
523
+ test("is named and covers docker only", () => {
524
+ expect(DockerEngineCommandPolicy.name).toBe("docker-engine");
525
+ expect([...DockerEngineCommandPolicy.programs]).toEqual(["docker"]);
526
+ expect(DOCKER_ENGINE_PROFILE.name).toBe("docker-engine");
527
+ });
528
+
529
+ test.each(
530
+ DOCKER_TYPES.map((type: AiResourceType) => {
531
+ return [type];
532
+ }),
533
+ )("%s routes to it", (type: AiResourceType) => {
534
+ expect(ResourceCommandPolicy.getToolPolicy(type)).toBe(
535
+ DockerEngineCommandPolicy,
536
+ );
537
+ });
538
+
539
+ test("its write table covers exactly the engine's write paths", () => {
540
+ expect([...DOCKER_ENGINE_WRITE_COMMANDS.keys()].sort()).toEqual(
541
+ [...DOCKER_ENGINE_WRITE_PATHS].sort(),
542
+ );
543
+ });
544
+
545
+ test("every read command in the table has an example below", () => {
546
+ const covered: Set<string> = new Set<string>(
547
+ READS.map((row: [string, string]): string => {
548
+ return row[1];
549
+ }),
550
+ );
551
+
552
+ for (const spec of DOCKER_ENGINE_READ_COMMANDS.values()) {
553
+ expect(covered.has(spec.verb)).toBe(true);
554
+ }
555
+ });
556
+
557
+ test("every write command in the table has an example below", () => {
558
+ const covered: Set<string> = new Set<string>(
559
+ [...SAFE_WRITES, ...RISKY_WRITES].map(
560
+ (row: [string, string, Array<string>]): string => {
561
+ return row[1];
562
+ },
563
+ ),
564
+ );
565
+
566
+ for (const spec of DOCKER_ENGINE_WRITE_COMMANDS.values()) {
567
+ expect(covered.has(spec.verb)).toBe(true);
568
+ }
569
+ });
570
+ });
571
+
572
+ describe("docker-engine: reads", () => {
573
+ test.each(READS)("%p is Read (%s)", (command: string, verb: string) => {
574
+ for (const type of DOCKER_TYPES) {
575
+ const result: ResourceCommandPolicyResult = evaluate(command, type);
576
+
577
+ expect(result.reason).not.toContain("never allowed");
578
+ expect(result.tier).toBe(ResourceCommandTier.Read);
579
+ expect(result.verb).toBe(verb);
580
+ expect(result.targets).toEqual([]);
581
+ expect(result.requiresHuman).toBeUndefined();
582
+ expect(result.program).toBe("docker");
583
+ expect(
584
+ ResourceCommandPolicy.isReadOnly({ resourceType: type, command }),
585
+ ).toBe(true);
586
+ }
587
+ });
588
+
589
+ test.each(
590
+ DOCKER_TYPES.map((type: AiResourceType) => {
591
+ return [type];
592
+ }),
593
+ )("%s: every Test connection command is Read", (type: AiResourceType) => {
594
+ for (const command of AI_RESOURCE_TYPE_INFO[type].testCommands) {
595
+ expect(evaluate(command, type).tier).toBe(ResourceCommandTier.Read);
596
+ }
597
+ });
598
+ });
599
+
600
+ describe("docker-engine: SafeWrite — exactly one container", () => {
601
+ test.each(SAFE_WRITES)(
602
+ "%p is SafeWrite (%s) on %p",
603
+ (command: string, verb: string, targets: Array<string>) => {
604
+ for (const type of DOCKER_TYPES) {
605
+ const result: ResourceCommandPolicyResult = evaluate(command, type);
606
+
607
+ expect(result.tier).toBe(ResourceCommandTier.SafeWrite);
608
+ expect(result.verb).toBe(verb);
609
+ expect(result.targets).toEqual(targets);
610
+ expect(result.requiresHuman).toBeUndefined();
611
+ }
612
+ },
613
+ );
614
+ });
615
+
616
+ describe("docker-engine: RiskyWrite", () => {
617
+ test.each(RISKY_WRITES)(
618
+ "%p is RiskyWrite (%s) on %p",
619
+ (command: string, verb: string, targets: Array<string>) => {
620
+ for (const type of DOCKER_TYPES) {
621
+ const result: ResourceCommandPolicyResult = evaluate(command, type);
622
+
623
+ expect(result.tier).toBe(ResourceCommandTier.RiskyWrite);
624
+ expect(result.verb).toBe(verb);
625
+ expect(describeTargets(result.targets)).toBe(describeTargets(targets));
626
+ expect(result.requiresHuman).toBeUndefined();
627
+ }
628
+ },
629
+ );
630
+
631
+ test("a SafeWrite verb naming several containers says why it is risky", () => {
632
+ expect(evaluate("docker restart web api").reason).toContain(
633
+ "exactly one container",
634
+ );
635
+ expect(evaluate("docker start web api").reason).toContain(
636
+ "exactly one container",
637
+ );
638
+ });
639
+
640
+ test("kill names the signal it sends", () => {
641
+ expect(evaluate("docker kill web").reason).toContain("SIGKILL");
642
+ expect(evaluate("docker kill -s sigterm web").reason).toContain("SIGTERM");
643
+ expect(evaluate("docker kill -s HUP web").reason).not.toContain(
644
+ "without a clean shutdown",
645
+ );
646
+ });
647
+
648
+ test("update names what it changes", () => {
649
+ const reason: string = evaluate(
650
+ "docker update --cpus 2 -m 1g --restart always web",
651
+ ).reason;
652
+
653
+ expect(reason).toContain("--memory");
654
+ expect(reason).toContain("--cpus");
655
+ expect(reason).toContain("--restart");
656
+ });
657
+ });
658
+
659
+ describe("docker-engine: Denied", () => {
660
+ test.each(DENIED)("%p is Denied (%s)", (command: string, phrase: string) => {
661
+ for (const type of DOCKER_TYPES) {
662
+ const result: ResourceCommandPolicyResult = evaluate(command, type);
663
+
664
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
665
+ expect(result.reason).toContain(phrase);
666
+ expect(result.targets).toEqual([]);
667
+ expect(result.requiresHuman).toBeUndefined();
668
+ expect(
669
+ ResourceCommandPolicy.isReadOnly({ resourceType: type, command }),
670
+ ).toBe(false);
671
+ }
672
+ });
673
+
674
+ test("a refused command tells the model what IS allowed", () => {
675
+ for (const command of [
676
+ "docker exec web sh",
677
+ "docker foo",
678
+ "docker service ls",
679
+ "docker -H tcp://x ps",
680
+ "docker container",
681
+ ]) {
682
+ const reason: string = evaluate(command).reason;
683
+
684
+ expect(reason).toContain("Allowed for a Docker or Podman host");
685
+ expect(reason).toContain("docker logs --tail 200 NAME");
686
+ expect(reason).toContain("docker restart|start|unpause NAME");
687
+ }
688
+ });
689
+
690
+ test("a refused flag names the flags the command allows", () => {
691
+ const reason: string = evaluate("docker logs --bogus web").reason;
692
+
693
+ expect(reason).toContain("--bogus");
694
+ expect(reason).toContain("-n/--tail VALUE");
695
+ expect(reason).toContain("--since VALUE");
696
+ expect(reason).not.toContain("--follow");
697
+ });
698
+
699
+ test("a refused flag with no allowed alternatives says so", () => {
700
+ expect(evaluate("docker top -x web").reason).toContain("allows: none");
701
+ });
702
+
703
+ test("the verb of a Denied command is kept for the audit trail when known", () => {
704
+ expect(evaluate("docker logs -f web").verb).toBe("logs");
705
+ expect(evaluate("docker exec web sh").verb).toBe("");
706
+ });
707
+
708
+ test("a flag in a short cluster is named with the word it hides in", () => {
709
+ expect(evaluate("docker logs -tf --tail 5 web").reason).toContain(
710
+ '-f (in "-tf")',
711
+ );
712
+ });
713
+ });
714
+
715
+ describe("docker-engine: smuggling attempts", () => {
716
+ test.each([
717
+ // A value flag swallowing the next word, as docker does.
718
+ [["docker", "logs", "--since", "--follow", "web"]],
719
+ [["docker", "ps", "--filter", "--format={{.Names}}"]],
720
+ [["docker", "kill", "-s", "-9", "web"]],
721
+ [["docker", "update", "--memory", "--privileged", "web"]],
722
+ // A switch never swallows: its "value" becomes a positional.
723
+ [["docker", "ps", "--all", "false"]],
724
+ [["docker", "restart", "-t", "5", "web", "--", "--privileged"]],
725
+ // Unicode look-alikes are never flags nor names.
726
+ [["docker", "logs", "—tail", "10", "web"]],
727
+ [["docker", "restart", "–t", "5", "web"]],
728
+ [["docker", "restart", "-t", "web"]],
729
+ [["docker", "ps", "-а"]],
730
+ [["docker", "rеstart", "web"]],
731
+ [["docker", "restart", "web​"]],
732
+ [["docker", "restart", "web "]],
733
+ [["docker", "ps​"]],
734
+ // Empty words.
735
+ [["docker", ""]],
736
+ [["docker", "", "ps"]],
737
+ [["docker", "ps", ""]],
738
+ [["docker", "logs", "--tail", "", "web"]],
739
+ [["docker", "restart", ""]],
740
+ // Very long words.
741
+ [["docker", "restart", "a".repeat(256)]],
742
+ [["docker", "logs", "--tail", "1".repeat(50), "web"]],
743
+ [["docker", "logs", "--since", "1h".repeat(40), "web"]],
744
+ // Quotes survive tokenizing as part of the word.
745
+ [["docker", "restart", "'web'"]],
746
+ [["docker", "restart", '"web"']],
747
+ // Case.
748
+ [["docker", "LOGS", "--tail", "5", "web"]],
749
+ [["docker", "logs", "--TAIL", "5", "web"]],
750
+ [["docker", "logs", "-N", "5", "web"]],
751
+ ])("%p is Denied", (argv: Array<string>) => {
752
+ const result: ResourceCommandPolicyResult = evaluateArgv(argv);
753
+
754
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
755
+ expect(result.targets).toEqual([]);
756
+ });
757
+
758
+ test("flags after positionals are read exactly as docker reads them", () => {
759
+ expect(evaluate("docker restart web -t 5").tier).toBe(
760
+ ResourceCommandTier.SafeWrite,
761
+ );
762
+ expect(evaluate("docker restart web -t 5 api").targets).toEqual([
763
+ "web",
764
+ "api",
765
+ ]);
766
+ expect(evaluate("docker logs web -f --tail 5").tier).toBe(
767
+ ResourceCommandTier.Denied,
768
+ );
769
+ });
770
+
771
+ test('"--" ends the flags: a flag after it is a (bad) container name', () => {
772
+ const result: ResourceCommandPolicyResult = evaluate(
773
+ "docker restart -- web --privileged",
774
+ );
775
+
776
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
777
+ expect(result.reason).toContain('"--privileged" is not a container name');
778
+ });
779
+
780
+ test("a cluster whose value letter takes the rest of the word", () => {
781
+ // -n takes "5a" as its value: not a number, so denied, not "-n 5 -a".
782
+ expect(evaluate("docker ps -n5a").reason).toContain("-n/--last");
783
+ // -a then -n with the next word.
784
+ expect(evaluate("docker ps -an 5").tier).toBe(ResourceCommandTier.Read);
785
+ // -n at the end of a cluster with nothing after it.
786
+ expect(evaluate("docker ps -an").reason).toContain("needs a value");
787
+ });
788
+
789
+ test('a switch never takes the next word: "--no-stream false" still streams nothing', () => {
790
+ const result: ResourceCommandPolicyResult = evaluateArgv([
791
+ "docker",
792
+ "stats",
793
+ "--no-stream",
794
+ "false",
795
+ ]);
796
+
797
+ // docker reads "false" as a container name, and --no-stream stays on.
798
+ expect(result.tier).toBe(ResourceCommandTier.Read);
799
+ expect(result.args).toEqual(["stats", "--no-stream", "false"]);
800
+ });
801
+
802
+ test("docker top stops reading flags at the container", () => {
803
+ expect(evaluate("docker top web --privileged").reason).toContain(
804
+ "no ps options",
805
+ );
806
+ });
807
+
808
+ test("the dispatcher refuses shell syntax before the policy sees it", () => {
809
+ for (const command of [
810
+ "docker ps | grep web",
811
+ "docker restart web; docker rm web",
812
+ "docker logs --tail 5 web > /tmp/x",
813
+ "docker restart $(docker ps -q)",
814
+ "docker restart `docker ps -q`",
815
+ "docker ps && docker rm web",
816
+ ]) {
817
+ const result: ResourceCommandPolicyResult = evaluate(command);
818
+
819
+ expect(result.tier).toBe(ResourceCommandTier.Denied);
820
+ expect(result.reason).toContain("pipes and redirects are not supported");
821
+ }
822
+ });
823
+
824
+ test("sudo and a program path are refused before the policy sees them", () => {
825
+ expect(evaluate("sudo docker ps").tier).toBe(ResourceCommandTier.Denied);
826
+ expect(evaluate("/usr/bin/docker ps").reason).toContain("without a path");
827
+ expect(evaluate("podman ps", AiResourceType.PodmanHost).reason).toContain(
828
+ "is not a program the Podman AI agent runs",
829
+ );
830
+ });
831
+ });
832
+
833
+ describe("docker-engine: evaluateArgv is total and fail-closed", () => {
834
+ test.each([
835
+ [null],
836
+ [undefined],
837
+ ["docker ps"],
838
+ [42],
839
+ [{}],
840
+ [[]],
841
+ [["docker", 7]],
842
+ [["docker", null, "ps"]],
843
+ [["kubectl", "get", "pods"]],
844
+ [["docker "]],
845
+ [["Docker", "ps"]],
846
+ [
847
+ Array.from({ length: 200 }, (): string => {
848
+ return "ps";
849
+ }),
850
+ ],
851
+ [
852
+ [
853
+ "docker",
854
+ "restart",
855
+ ...Array.from({ length: 70 }, (): string => {
856
+ return "web";
857
+ }),
858
+ ],
859
+ ],
860
+ ])("%p is Denied without throwing", (argv: unknown) => {
861
+ let result: ResourceCommandPolicyResult | undefined;
862
+
863
+ expect(() => {
864
+ result = DockerEngineCommandPolicy.evaluateArgv(argv as Array<string>);
865
+ }).not.toThrow();
866
+ expect(result && result.tier).toBe(ResourceCommandTier.Denied);
867
+ expect(result && result.targets).toEqual([]);
868
+ });
869
+
870
+ test("it never changes the argv it is given", () => {
871
+ const argv: Array<string> = ["docker", "restart", "-t", "5", "web"];
872
+
873
+ DockerEngineCommandPolicy.evaluateArgv(argv);
874
+ expect(argv).toEqual(["docker", "restart", "-t", "5", "web"]);
875
+ });
876
+
877
+ test("the program must be docker", () => {
878
+ expect(
879
+ DockerEngineCommandPolicy.evaluateArgv(["podman", "ps"]).reason,
880
+ ).toContain('start the command with "docker"');
881
+ });
882
+ });
883
+
884
+ describe("docker-engine: the result's shape", () => {
885
+ test("args are the words after the program, exactly as written", () => {
886
+ const result: ResourceCommandPolicyResult = evaluate(
887
+ "docker container list -aq --filter 'name=web'",
888
+ );
889
+
890
+ expect(result.args).toEqual([
891
+ "container",
892
+ "list",
893
+ "-aq",
894
+ "--filter",
895
+ "name=web",
896
+ ]);
897
+ expect(result.verb).toBe("container ls");
898
+ });
899
+
900
+ test.each([
901
+ ["docker restart web", "docker restart web"],
902
+ ["docker restart 'web'", "docker restart web"],
903
+ [
904
+ "docker ps --filter 'label=com.example.team=a b'",
905
+ "docker ps --filter 'label=com.example.team=a b'",
906
+ ],
907
+ ["docker ps --format '{{.Names}}'", "docker ps --format '{{.Names}}'"],
908
+ ['docker logs --tail 10 "my app"', "docker logs --tail 10 'my app'"],
909
+ ["docker restart ''", "docker restart ''"],
910
+ ])("%p is displayed as %p", (command: string, display: string) => {
911
+ const result: ResourceCommandPolicyResult = evaluate(command);
912
+
913
+ expect(result.displayCommand).toBe(display);
914
+ // The display round-trips through the tokenizer.
915
+ expect(tokenizeResourceCommand(result.displayCommand).argv).toEqual(
916
+ tokenizeResourceCommand(command).argv,
917
+ );
918
+ });
919
+
920
+ test("reads have no targets; writes name their containers once each", () => {
921
+ expect(evaluate("docker logs --tail 5 web").targets).toEqual([]);
922
+ expect(evaluate("docker stats --no-stream web api").targets).toEqual([]);
923
+ expect(evaluate("docker stop web api web").targets).toEqual(["web", "api"]);
924
+ });
925
+
926
+ test("the kill-signal normalizer reads names as the engine does", () => {
927
+ expect(normalizeDockerKillSignal("SIGTERM")).toBe("TERM");
928
+ expect(normalizeDockerKillSignal("term")).toBe("TERM");
929
+ expect(normalizeDockerKillSignal("SigHup")).toBe("HUP");
930
+ expect(normalizeDockerKillSignal("9")).toBeNull();
931
+ expect(normalizeDockerKillSignal("SIGSTOP")).toBeNull();
932
+ expect(normalizeDockerKillSignal(7 as unknown as string)).toBeNull();
933
+ expect(DOCKER_KILL_SIGNALS).toContain("KILL");
934
+ });
935
+ });
936
+
937
+ describe("docker-engine: the auto-execution ladder through the dispatcher", () => {
938
+ function verdictFor(data: {
939
+ command: string;
940
+ allowlistPatterns?: Array<string>;
941
+ bypassApproval?: boolean;
942
+ resourceType?: AiResourceType;
943
+ }): ResourceAutoExecutionVerdict {
944
+ return ResourceCommandPolicy.evaluateForAutoExecution({
945
+ resourceType: data.resourceType ?? AiResourceType.DockerHost,
946
+ command: data.command,
947
+ allowlistPatterns: data.allowlistPatterns ?? [],
948
+ bypassApproval: data.bypassApproval ?? false,
949
+ });
950
+ }
951
+
952
+ test("a read is AutoApproved (it changes nothing)", () => {
953
+ expect(verdictFor({ command: "docker ps -a" }).verdict).toBe(
954
+ AiRemediationCommandPolicyVerdict.AutoApproved,
955
+ );
956
+ });
957
+
958
+ test.each(
959
+ DOCKER_TYPES.map((type: AiResourceType) => {
960
+ return [type];
961
+ }),
962
+ )(
963
+ "%s: a restart of one container is AutoApproved",
964
+ (type: AiResourceType) => {
965
+ const verdict: ResourceAutoExecutionVerdict = verdictFor({
966
+ command: "docker restart web",
967
+ resourceType: type,
968
+ });
969
+
970
+ expect(verdict.verdict).toBe(
971
+ AiRemediationCommandPolicyVerdict.AutoApproved,
972
+ );
973
+ expect(verdict.tier).toBe(ResourceCommandTier.SafeWrite);
974
+ },
975
+ );
976
+
977
+ test("a restart of several containers needs approval", () => {
978
+ expect(verdictFor({ command: "docker restart web api" }).verdict).toBe(
979
+ AiRemediationCommandPolicyVerdict.RequiresApproval,
980
+ );
981
+ });
982
+
983
+ test("a stop needs approval, unless bypassed or allowlisted", () => {
984
+ expect(verdictFor({ command: "docker stop web" }).verdict).toBe(
985
+ AiRemediationCommandPolicyVerdict.RequiresApproval,
986
+ );
987
+ expect(
988
+ verdictFor({ command: "docker stop web", bypassApproval: true }).verdict,
989
+ ).toBe(AiRemediationCommandPolicyVerdict.AutoApproved);
990
+ expect(
991
+ verdictFor({
992
+ command: "docker stop web",
993
+ allowlistPatterns: ["docker stop web"],
994
+ }).verdict,
995
+ ).toBe(AiRemediationCommandPolicyVerdict.AutoApproved);
996
+ expect(
997
+ verdictFor({
998
+ command: "docker stop web",
999
+ allowlistPatterns: ["docker stop *"],
1000
+ }).verdict,
1001
+ ).toBe(AiRemediationCommandPolicyVerdict.AutoApproved);
1002
+ expect(
1003
+ verdictFor({
1004
+ command: "docker stop web",
1005
+ allowlistPatterns: ["docker stop api"],
1006
+ }).verdict,
1007
+ ).toBe(AiRemediationCommandPolicyVerdict.RequiresApproval);
1008
+ });
1009
+
1010
+ test("a Denied command stays Denied whatever the settings", () => {
1011
+ const verdict: ResourceAutoExecutionVerdict = verdictFor({
1012
+ command: "docker exec web sh",
1013
+ allowlistPatterns: ["docker exec * sh"],
1014
+ bypassApproval: true,
1015
+ });
1016
+
1017
+ expect(verdict.verdict).toBe(AiRemediationCommandPolicyVerdict.Denied);
1018
+ expect(verdict.reason).toContain("cannot run even with human approval");
1019
+ });
1020
+
1021
+ test("no docker-engine write asks for a human by itself", () => {
1022
+ for (const [command] of [...SAFE_WRITES, ...RISKY_WRITES]) {
1023
+ expect(verdictFor({ command, bypassApproval: true }).verdict).toBe(
1024
+ AiRemediationCommandPolicyVerdict.AutoApproved,
1025
+ );
1026
+ }
1027
+ });
1028
+ });
1029
+
1030
+ describe("docker-engine: allowlist entries", () => {
1031
+ function problem(pattern: unknown): string | null {
1032
+ return ResourceCommandPolicy.describeAllowlistPatternProblem({
1033
+ resourceType: AiResourceType.DockerHost,
1034
+ pattern,
1035
+ });
1036
+ }
1037
+
1038
+ function isBroad(pattern: string): boolean {
1039
+ return ResourceCommandPolicy.isBroadAllowlistPattern({
1040
+ resourceType: AiResourceType.DockerHost,
1041
+ pattern,
1042
+ });
1043
+ }
1044
+
1045
+ test.each([
1046
+ ["docker stop *"],
1047
+ ["docker stop web"],
1048
+ ["docker restart *"],
1049
+ ["docker kill -s TERM *"],
1050
+ ["docker update --memory * web"],
1051
+ ["docker update --memory 1g *"],
1052
+ ["docker pause *"],
1053
+ ["docker container stop *"],
1054
+ ])("%p is a valid entry", (pattern: string) => {
1055
+ expect(problem(pattern)).toBeNull();
1056
+ });
1057
+
1058
+ test.each([
1059
+ ["docker ps -a", "read-only command"],
1060
+ ["docker logs --tail 10 *", "read-only command"],
1061
+ ["docker exec * sh", "can never match"],
1062
+ ["docker kill -s * web", "can never match"],
1063
+ ["docker service update --force *", "can never match"],
1064
+ ["docker * web", "where the command goes"],
1065
+ ["docker stop", "fewer than two words"],
1066
+ ["podman stop *", "does not start with a program"],
1067
+ ["docker stop * | xargs", "cannot be read as one command"],
1068
+ ["docker -H tcp://x stop *", "can never match"],
1069
+ ])("%p is refused (%s)", (pattern: string, phrase: string) => {
1070
+ expect(problem(pattern)).toContain(phrase);
1071
+ });
1072
+
1073
+ test("an entry with a * for the container is broad; one naming it is not", () => {
1074
+ expect(isBroad("docker stop *")).toBe(true);
1075
+ expect(isBroad("docker update --memory 1g *")).toBe(true);
1076
+ expect(isBroad("docker update --memory * web")).toBe(false);
1077
+ expect(isBroad("docker stop web")).toBe(false);
1078
+ expect(isBroad("docker ps *")).toBe(false);
1079
+ });
1080
+
1081
+ test("matching is word by word", () => {
1082
+ const matches: (command: string, patterns: Array<string>) => boolean = (
1083
+ command: string,
1084
+ patterns: Array<string>,
1085
+ ): boolean => {
1086
+ return ResourceCommandPolicy.matchesAllowlist({
1087
+ resourceType: AiResourceType.DockerHost,
1088
+ command,
1089
+ patterns,
1090
+ });
1091
+ };
1092
+
1093
+ expect(matches("docker kill -s TERM web", ["docker kill -s TERM *"])).toBe(
1094
+ true,
1095
+ );
1096
+ expect(matches("docker kill -s TERM web", ["docker kill *"])).toBe(false);
1097
+ expect(matches("docker stop -t 5 web", ["docker stop *"])).toBe(false);
1098
+ expect(matches("docker stop web api", ["docker stop *"])).toBe(false);
1099
+ // An entry that can never pre-approve anything is skipped.
1100
+ expect(matches("docker exec web sh", ["docker exec * sh"])).toBe(false);
1101
+ });
1102
+ });
1103
+
1104
+ describe("docker-engine: the agent's write scope", () => {
1105
+ function refusal(
1106
+ command: string,
1107
+ posture: {
1108
+ allowWrites?: boolean;
1109
+ writeTargets?: Array<string>;
1110
+ protectedTargets?: Array<string>;
1111
+ } = {},
1112
+ ): string | null {
1113
+ return ResourceCommandPolicy.getWriteScopeRefusal({
1114
+ result: evaluate(command),
1115
+ allowWrites: posture.allowWrites ?? true,
1116
+ writeTargets: posture.writeTargets ?? [],
1117
+ protectedTargets: posture.protectedTargets ?? [],
1118
+ resourceType: AiResourceType.DockerHost,
1119
+ });
1120
+ }
1121
+
1122
+ test("a read is never refused, even by a read-only agent", () => {
1123
+ expect(refusal("docker ps", { allowWrites: false })).toBeNull();
1124
+ });
1125
+
1126
+ test("a read-only agent refuses every write", () => {
1127
+ expect(refusal("docker restart web", { allowWrites: false })).toContain(
1128
+ "ONEUPTIME_AI_ALLOW_WRITES=true",
1129
+ );
1130
+ });
1131
+
1132
+ test("the agent's own container is protected, by name or id prefix", () => {
1133
+ const protectedTargets: Array<string> = [
1134
+ "oneuptime-ai-agent",
1135
+ "3f2a9c1b7d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8",
1136
+ ];
1137
+
1138
+ expect(
1139
+ refusal("docker restart oneuptime-ai-agent", { protectedTargets }),
1140
+ ).toContain("protects");
1141
+ expect(refusal("docker stop 3f2a", { protectedTargets })).toContain(
1142
+ "protects",
1143
+ );
1144
+ expect(
1145
+ refusal("docker restart web oneuptime-ai-agent", { protectedTargets }),
1146
+ ).toContain("protects");
1147
+ expect(refusal("docker restart web", { protectedTargets })).toBeNull();
1148
+ });
1149
+
1150
+ test("ONEUPTIME_AI_WRITE_TARGETS bounds every target", () => {
1151
+ expect(
1152
+ refusal("docker restart web-1", { writeTargets: ["web-*"] }),
1153
+ ).toBeNull();
1154
+ expect(
1155
+ refusal("docker stop web-1 db", { writeTargets: ["web-*"] }),
1156
+ ).toContain("ONEUPTIME_AI_WRITE_TARGETS");
1157
+ });
1158
+
1159
+ test("a Denied command is refused", () => {
1160
+ expect(refusal("docker exec web sh")).toContain("denied");
1161
+ });
1162
+ });
1163
+
1164
+ describe("docker-engine: command guides", () => {
1165
+ test("the read guide lists the bounded reads as markdown bullets", () => {
1166
+ const guide: string = DockerEngineCommandPolicy.readCommandGuide;
1167
+
1168
+ for (const line of guide.split("\n")) {
1169
+ expect(line.startsWith("- ")).toBe(true);
1170
+ }
1171
+
1172
+ for (const phrase of [
1173
+ "docker ps",
1174
+ "docker container inspect NAME",
1175
+ "--tail",
1176
+ "2000",
1177
+ "--since",
1178
+ "--no-stream",
1179
+ "docker events --since 30m --until 0s",
1180
+ "docker info",
1181
+ "--format json",
1182
+ ]) {
1183
+ expect(guide).toContain(phrase);
1184
+ }
1185
+ });
1186
+
1187
+ test("every command the read guide suggests is actually Read", () => {
1188
+ for (const command of [
1189
+ "docker ps -a",
1190
+ "docker container inspect NAME",
1191
+ "docker inspect --type container NAME",
1192
+ "docker logs --tail 200 NAME",
1193
+ "docker logs --since 30m NAME",
1194
+ "docker stats --no-stream",
1195
+ "docker top NAME",
1196
+ "docker port NAME",
1197
+ "docker diff NAME",
1198
+ "docker events --since 30m --until 0s",
1199
+ "docker info",
1200
+ "docker version",
1201
+ "docker system df -v",
1202
+ "docker images",
1203
+ "docker image inspect IMAGE",
1204
+ "docker network ls",
1205
+ "docker network inspect NAME",
1206
+ "docker volume ls",
1207
+ "docker volume inspect NAME",
1208
+ ]) {
1209
+ expect(evaluate(command).tier).toBe(ResourceCommandTier.Read);
1210
+ }
1211
+ });
1212
+
1213
+ test("every change the write guide suggests has the tier it claims", () => {
1214
+ for (const command of [
1215
+ "docker restart NAME",
1216
+ "docker restart -t 10 NAME",
1217
+ "docker start NAME",
1218
+ "docker unpause NAME",
1219
+ ]) {
1220
+ expect(evaluate(command).tier).toBe(ResourceCommandTier.SafeWrite);
1221
+ }
1222
+
1223
+ for (const command of [
1224
+ "docker stop NAME",
1225
+ "docker stop -t 10 NAME",
1226
+ "docker kill -s TERM NAME",
1227
+ "docker pause NAME",
1228
+ "docker update --memory 1g NAME",
1229
+ "docker update --cpus 1.5 NAME",
1230
+ "docker update --restart on-failure:3 NAME",
1231
+ ]) {
1232
+ expect(evaluate(command).tier).toBe(ResourceCommandTier.RiskyWrite);
1233
+ }
1234
+ });
1235
+
1236
+ test("the write guide names the tiers and the never-list", () => {
1237
+ const guide: string = DockerEngineCommandPolicy.writeCommandGuide;
1238
+
1239
+ expect(guide).toContain("Safe");
1240
+ expect(guide).toContain("Risky");
1241
+ expect(guide).toContain("Never");
1242
+ expect(guide).toContain("exec");
1243
+
1244
+ for (const line of guide.split("\n")) {
1245
+ expect(line.startsWith("- ")).toBe(true);
1246
+ }
1247
+ });
1248
+
1249
+ test("the dispatcher serves these guides for Docker and Podman hosts", () => {
1250
+ for (const type of DOCKER_TYPES) {
1251
+ expect(ResourceCommandPolicy.getReadCommandGuide(type)).toBe(
1252
+ DockerEngineCommandPolicy.readCommandGuide,
1253
+ );
1254
+ expect(ResourceCommandPolicy.getWriteCommandGuide(type)).toBe(
1255
+ DockerEngineCommandPolicy.writeCommandGuide,
1256
+ );
1257
+ }
1258
+ });
1259
+ });