harness-mcp-v2 3.2.19 → 3.2.21

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 (111) hide show
  1. package/README.md +146 -75
  2. package/build/client/harness-client.d.ts.map +1 -1
  3. package/build/client/harness-client.js +32 -6
  4. package/build/client/harness-client.js.map +1 -1
  5. package/build/config.d.ts +10 -0
  6. package/build/config.d.ts.map +1 -1
  7. package/build/config.js +15 -0
  8. package/build/config.js.map +1 -1
  9. package/build/data/schemas/v0/pipeline.d.ts.map +1 -1
  10. package/build/data/schemas/v0/pipeline.js +1388 -580
  11. package/build/data/schemas/v0/pipeline.js.map +1 -1
  12. package/build/data/schemas/v0/template.d.ts.map +1 -1
  13. package/build/data/schemas/v0/template.js +1142 -166
  14. package/build/data/schemas/v0/template.js.map +1 -1
  15. package/build/data/schemas/v1/pipeline.d.ts.map +1 -1
  16. package/build/data/schemas/v1/pipeline.js +302 -90
  17. package/build/data/schemas/v1/pipeline.js.map +1 -1
  18. package/build/data/schemas/v1/template.d.ts.map +1 -1
  19. package/build/data/schemas/v1/template.js +708 -488
  20. package/build/data/schemas/v1/template.js.map +1 -1
  21. package/build/prompts/deploy-ai-agent.d.ts +3 -0
  22. package/build/prompts/deploy-ai-agent.d.ts.map +1 -0
  23. package/build/prompts/deploy-ai-agent.js +300 -0
  24. package/build/prompts/deploy-ai-agent.js.map +1 -0
  25. package/build/prompts/feature-flag-rollout.d.ts.map +1 -1
  26. package/build/prompts/feature-flag-rollout.js +19 -8
  27. package/build/prompts/feature-flag-rollout.js.map +1 -1
  28. package/build/prompts/index.d.ts.map +1 -1
  29. package/build/prompts/index.js +2 -0
  30. package/build/prompts/index.js.map +1 -1
  31. package/build/registry/extractors.d.ts +87 -10
  32. package/build/registry/extractors.d.ts.map +1 -1
  33. package/build/registry/extractors.js +426 -26
  34. package/build/registry/extractors.js.map +1 -1
  35. package/build/registry/index.d.ts.map +1 -1
  36. package/build/registry/index.js +34 -15
  37. package/build/registry/index.js.map +1 -1
  38. package/build/registry/scope-utils.d.ts +45 -0
  39. package/build/registry/scope-utils.d.ts.map +1 -1
  40. package/build/registry/scope-utils.js +78 -0
  41. package/build/registry/scope-utils.js.map +1 -1
  42. package/build/registry/toolsets/chaos-descriptions.d.ts +72 -20
  43. package/build/registry/toolsets/chaos-descriptions.d.ts.map +1 -1
  44. package/build/registry/toolsets/chaos-descriptions.js +435 -140
  45. package/build/registry/toolsets/chaos-descriptions.js.map +1 -1
  46. package/build/registry/toolsets/chaos.d.ts.map +1 -1
  47. package/build/registry/toolsets/chaos.js +967 -193
  48. package/build/registry/toolsets/chaos.js.map +1 -1
  49. package/build/registry/toolsets/feature-flags.d.ts.map +1 -1
  50. package/build/registry/toolsets/feature-flags.js +888 -85
  51. package/build/registry/toolsets/feature-flags.js.map +1 -1
  52. package/build/registry/toolsets/governance.d.ts.map +1 -1
  53. package/build/registry/toolsets/governance.js +4 -0
  54. package/build/registry/toolsets/governance.js.map +1 -1
  55. package/build/registry/toolsets/iacm.d.ts.map +1 -1
  56. package/build/registry/toolsets/iacm.js +787 -11
  57. package/build/registry/toolsets/iacm.js.map +1 -1
  58. package/build/registry/toolsets/infrastructure.d.ts.map +1 -1
  59. package/build/registry/toolsets/infrastructure.js +30 -5
  60. package/build/registry/toolsets/infrastructure.js.map +1 -1
  61. package/build/registry/toolsets/pipelines.d.ts.map +1 -1
  62. package/build/registry/toolsets/pipelines.js +208 -5
  63. package/build/registry/toolsets/pipelines.js.map +1 -1
  64. package/build/registry/toolsets/release-management.d.ts +8 -0
  65. package/build/registry/toolsets/release-management.d.ts.map +1 -0
  66. package/build/registry/toolsets/release-management.js +783 -0
  67. package/build/registry/toolsets/release-management.js.map +1 -0
  68. package/build/registry/toolsets/sto.d.ts.map +1 -1
  69. package/build/registry/toolsets/sto.js +93 -17
  70. package/build/registry/toolsets/sto.js.map +1 -1
  71. package/build/registry/types.d.ts +52 -4
  72. package/build/registry/types.d.ts.map +1 -1
  73. package/build/resources/harness-schema.d.ts.map +1 -1
  74. package/build/resources/harness-schema.js +2 -1
  75. package/build/resources/harness-schema.js.map +1 -1
  76. package/build/tools/entity-schema/live.d.ts +6 -5
  77. package/build/tools/entity-schema/live.d.ts.map +1 -1
  78. package/build/tools/entity-schema/live.js +114 -10
  79. package/build/tools/entity-schema/live.js.map +1 -1
  80. package/build/tools/entity-schema/types.d.ts +7 -1
  81. package/build/tools/entity-schema/types.d.ts.map +1 -1
  82. package/build/tools/harness-create.d.ts.map +1 -1
  83. package/build/tools/harness-create.js +5 -1
  84. package/build/tools/harness-create.js.map +1 -1
  85. package/build/tools/harness-delete.d.ts.map +1 -1
  86. package/build/tools/harness-delete.js +5 -1
  87. package/build/tools/harness-delete.js.map +1 -1
  88. package/build/tools/harness-execute.d.ts.map +1 -1
  89. package/build/tools/harness-execute.js +15 -9
  90. package/build/tools/harness-execute.js.map +1 -1
  91. package/build/tools/harness-get.d.ts.map +1 -1
  92. package/build/tools/harness-get.js +5 -1
  93. package/build/tools/harness-get.js.map +1 -1
  94. package/build/tools/harness-list.d.ts.map +1 -1
  95. package/build/tools/harness-list.js +18 -2
  96. package/build/tools/harness-list.js.map +1 -1
  97. package/build/tools/harness-schema.d.ts +2 -1
  98. package/build/tools/harness-schema.d.ts.map +1 -1
  99. package/build/tools/harness-schema.js +10 -8
  100. package/build/tools/harness-schema.js.map +1 -1
  101. package/build/tools/harness-update.d.ts.map +1 -1
  102. package/build/tools/harness-update.js +5 -1
  103. package/build/tools/harness-update.js.map +1 -1
  104. package/build/tools/index.js +1 -1
  105. package/build/tools/index.js.map +1 -1
  106. package/build/utils/url-parser.d.ts +2 -0
  107. package/build/utils/url-parser.d.ts.map +1 -1
  108. package/build/utils/url-parser.js +55 -0
  109. package/build/utils/url-parser.js.map +1 -1
  110. package/npm-shrinkwrap.json +2 -2
  111. package/package.json +1 -1
@@ -1,17 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import YAML from "yaml";
3
- import { passthrough, ngExtract, chaosPageExtract, chaosHeatmapExtract, chaosScannedRiskGetExtract, chaosExperimentListExtract, chaosInputSetListExtract, chaosAppMapPageExtract, chaosProbeListExtract, chaosInfraListExtract, chaosK8sInfraListExtract, chaosLoadTestListExtract, chaosLoadTestExtract, chaosHubListExtract, chaosDRTestListExtract, sdPageExtract, chaosRunTimeInputsExtract, chaosActionExtract, } from "../extractors.js";
3
+ import { passthrough, ngExtract, chaosPageExtract, chaosHeatmapExtract, chaosScannedRiskGetExtract, chaosExperimentListExtract, chaosInputSetListExtract, chaosAppMapPageExtract, chaosProbeListExtract, chaosInfraListExtract, chaosK8sInfraListExtract, chaosLoadTestListExtract, chaosLoadTestExtract, chaosServiceListExtract, chaosHubListExtract, chaosDRTestListExtract, sdPageExtract, chaosRunTimeInputsExtract, chaosActionExtract, } from "../extractors.js";
4
4
  import { descToolsetChaos,
5
5
  // Resource descriptions
6
- descChaosExperiment, descChaosExperimentRun, descChaosProbe, descChaosExperimentTemplate, descChaosExperimentVariable, descChaosInfrastructure, descChaosLoadtest, descChaosK8sInfrastructure, descChaosEnabledInfrastructure, descChaosHub, descChaosFault, descChaosFaultExperimentRun, descChaosFaultTemplate, descChaosProbeTemplate, descChaosActionTemplate, descChaosHubFault, descChaosEnvironment, descChaosApplicationMap, descDiscoveredNetworkMap, descChaosGuardCondition, descChaosGuardRule, descChaosRecommendation, descChaosRisk, descChaosAction, descChaosProbeInRun, descChaosDRTest, descChaosComponentVariable,
6
+ descChaosExperiment, descChaosExperimentRun, descChaosProbe, descChaosExperimentTemplate, descChaosExperimentVariable, descChaosInfrastructure, descChaosLoadtest, descChaosService, descChaosK8sInfrastructure, descChaosEnabledInfrastructure, descChaosHub, descChaosFault, descChaosFaultExperimentRun, descChaosFaultTemplate, descChaosProbeTemplate, descChaosActionTemplate, descChaosHubFault, descChaosEnvironment, descChaosApplicationMap, descDiscoveredNetworkMap, descChaosGuardCondition, descChaosGuardRule, descChaosRecommendation, descChaosRisk, descChaosAction, descChaosProbeInRun, descChaosDRTest, descChaosComponentVariable,
7
7
  // Operation descriptions
8
- descListExperiments, descGetExperiment, descGetExperimentRun, descListProbes, descGetProbe, descCreateProbe, descListExperimentTemplates, descGetExperimentTemplate, descDeleteExperimentTemplate, descListExperimentVariables, descGetComponentVariable, descCreateExperiment, descListLinuxInfra, descListLoadtests, descGetLoadtest, descCreateLoadtest, descDeleteLoadtest, descListK8sInfra, descGetK8sInfra, descListChaosEnabledInfra, descListHubs, descGetHub, descCreateHub, descUpdateHub, descDeleteHub, descListFaults, descGetFault, descListFaultTemplates, descGetFaultTemplate, descDeleteFaultTemplate, descListProbeTemplates, descGetProbeTemplate, descDeleteProbeTemplate, descListActionTemplates, descGetActionTemplate, descDeleteActionTemplate, descListHubFaults, descListChaosEnvironments, descListApplicationMaps, descGetApplicationMap, descAppMapSearch, descAppMapEnvironmentId, descAppMapInfraId, descAppMapAll, descAppMapMinimal, descListDiscoveredNetworkMaps, descSDNetworkMapSearch, descListGuardConditions, descGetGuardCondition, descDeleteGuardCondition, descListGuardRules, descGetGuardRule, descDeleteGuardRule, descListRecommendations, descGetRecommendation, descListRisks, descGetRisk, descListDRTests, descDeleteProbe, descGetProbeManifest, descListProbesInRun, descGetFaultVariables, descGetFaultYaml, descListFaultExperimentRuns, descDeleteFault, descListActions, descGetAction, descGetActionManifest, descDeleteAction, descCreateAction, descBodyActionCreate, descActionName, descActionIdentityCreate, descActionEntityTypeCreate, descActionInfraTypeCreate, descActionPropertiesBody, descActionDurationShorthand, descActionDescriptionCreate, descActionTagsCreate, descActionVariablesBody, descActionRunPropertiesBody, descActionInputsBody,
8
+ descListExperiments, descGetExperiment, descGetExperimentRun, descListProbes, descGetProbe, descCreateProbe, descListExperimentTemplates, descGetExperimentTemplate, descDeleteExperimentTemplate, descListExperimentVariables, descGetComponentVariable, descCreateExperiment, descListLinuxInfra, descListLoadtests, descGetLoadtest, descCreateLoadtest, descUpdateLoadtest, descDeleteLoadtest, descListChaosServices, descGetChaosService, descDeleteChaosService, descCreateChaosService, descUpdateChaosService, descChaosServiceEnvironmentIds, descChaosServiceInfrastructureIds, descChaosServiceTags, descChaosServiceIncludeAllScope, descChaosServiceProbeIds, descChaosServiceOnboardingIdFilter, descListChaosServiceExperimentRuns, descListChaosServiceLoadTests, descChaosServiceSearch, descChaosServiceIdentity, descBodyChaosServiceCreate, descBodyChaosServiceUpdate, descChaosServiceName, descChaosServiceDescription, descChaosServiceTagsBody, descChaosServiceExternalServiceId, descChaosServiceAgentId, descChaosServiceEnvironmentId, descChaosServiceInfrastructureId, descChaosServiceInfrastructureType, descChaosServiceOnboardingId, descChaosServiceProbes, descChaosServiceProbesUpdate, descListK8sInfra, descGetK8sInfra, descCreateK8sInfra, descListChaosEnabledInfra, descBodyK8sInfraCreate, descK8sInfraIdentityCreate, descK8sInfraNameCreate, descK8sInfraEnvironmentIdCreate, descK8sInfraInfraIdCreate, descK8sInfraConnectorIdCreate, descK8sInfraNamespaceCreate, descK8sInfraServiceAccountCreate, descK8sInfraScopeCreate, descK8sInfraTypeCreate, descK8sInfraAiEnabledCreate, descListHubs, descGetHub, descCreateHub, descUpdateHub, descDeleteHub, descListFaults, descGetFault, descListFaultTemplates, descGetFaultTemplate, descDeleteFaultTemplate, descListProbeTemplates, descGetProbeTemplate, descDeleteProbeTemplate, descListActionTemplates, descGetActionTemplate, descDeleteActionTemplate, descListHubFaults, descListChaosEnvironments, descListApplicationMaps, descGetApplicationMap, descAppMapSearch, descAppMapEnvironmentId, descAppMapInfraId, descAppMapAll, descAppMapMinimal, descListDiscoveredNetworkMaps, descSDNetworkMapSearch, descListGuardConditions, descGetGuardCondition, descDeleteGuardCondition, descListGuardRules, descGetGuardRule, descDeleteGuardRule, descListRecommendations, descGetRecommendation, descListRisks, descGetRisk, descListDRTests, descDeleteProbe, descGetProbeManifest, descListProbesInRun, descGetFaultVariables, descGetFaultYaml, descListFaultExperimentRuns, descDeleteFault, descListActions, descGetAction, descGetActionManifest, descDeleteAction, descCreateAction, descBodyActionCreate, descActionName, descActionIdentityCreate, descActionEntityTypeCreate, descActionInfraTypeCreate, descActionPropertiesBody, descActionDurationShorthand, descActionDescriptionCreate, descActionTagsCreate, descActionVariablesBody, descActionRunPropertiesBody, descActionInputsBody,
9
9
  // Action descriptions
10
10
  descRunExperiment, descStopExperiment, descDeleteExperiment, descEnableProbe, descVerifyProbe, descCreateFromTemplate, descListRevisions, descGetVariables, descGetYaml, descCompareRevisions, descRunLoadtest, descStopLoadtest, descCheckK8sHealth, descEnableGuardRule, descGetProbeTemplateVariables, descListActionTemplateRevisions, descGetActionTemplateVariables, descCompareActionTemplateRevisions,
11
11
  // Body schema descriptions
12
12
  descBodyExperimentRun, descBodyNoBody, descBodyExperimentCreate, descBodyCreateFromTemplate, descBodyLoadtestDefinition, descBodyProbeEnable, descBodyProbeVerify, descBodyProbesInRun, descBodyProbeCreate,
13
13
  // Field descriptions
14
- descInputsetIdentity, descRuntimeInputs, descHubIdentity, descInfraType, descExperimentName, descExperimentIdentity, descInfraRef, descExperimentId, descInfraStatus, descLoadtestName, descLoadtestType, descLoadtestIdentity, descLoadtestDescription, descLoadtestTags, descLoadtestEnvId, descLoadtestInfraId, descLoadtestTargetType, descLoadtestTargetUrl, descLoadtestScript, descLoadtestUsers, descLoadtestDurationSec, descLoadtestRampUpSec, descLoadtestWorkerCount, descLoadtestScriptSource, descLoadtestScriptImage, descLoadtestScriptEntrypoint, descLoadtestLoadArgs, descLoadtestHostUrl, descLoadtestRpsLimit, descLoadtestIterations, descLoadtestEnvVars, descHubIdentityExact, descHubName, descHubNameUpdate, descHubDescription, descHubDescriptionUpdate, descHubTags, descHubTagsReplace, descConnectorRef, descRepoName, descRepoBranch, descHubSearch, descIncludeAllScope, descTemplateSearch, descSortField, descSortAsc, descTags, descInfrastructure, descTemplateIdentity, descRevision, descRevision1, descRevision2, descRevisionToCompare, descFaultType, descFaultCategory, descFaultPermissions, descFaultIsEnterprise, descImportType, descExperimentDescription, descExperimentTags, descEntityTypeProbe, descEntityTypeAction, descEntityTypeFault, descPermissionsRequiredEnum, descOnlyTemplatisedFaults, descEnvironmentId, descK8sInfraStatus, descIncludeLegacyInfra, descSearchK8sInfra, descChaosEnabledInfraType, descInfraScope, descInfraAiEnabled, descSearchTermEnv, descSortEnv, descEnvironmentType, descGuardSearch, descGuardInfraType, descGuardTags, descGuardEnabled, descExperimentRunIdStop, descNotifyId, descForce, descIsEnabledFlag, descIsBulkUpdate, descVerifyFlag, descExperimentRunIds, descNotifyIds, descFaultIdentityParam, descIsEnterpriseFilter, descFaultSearch, descFaultListType, descFaultListInfraType, descFaultListInfrastructure, descFaultListTags, descFaultListCategory, descFaultListSortField, descFaultListSortAscending, descIsEnterpriseYaml, descIsEnterpriseVars, descIsEnterpriseRuns, descActionIdentityParam, descSearchActionsParam, descHubIdentityActions, descExperimentVariablesParam, descTasksParam, descEnvironmentIdCreate, descInfraIdCreate, descAccountIdBody, descOrgIdBody, descProjectIdBody, descSearchExperiments, descExperimentInfraId, descExperimentEnvironmentId, descExperimentIds, descExperimentStartDate, descExperimentEndDate, descExperimentTargetNetworkMapIds, descExperimentMyExperiments, descExperimentExcludeAutomation, descSearchProbes, descProbeIds, descProbeSortField, descProbeIdField, descProbeNameField, descProbePropertiesField, descRunPropertiesField, descDRTestSort, descCreateDRTest, descBodyDRTestCreate, descDRTestName, descDRTestIdentifier, descDRTestDescription, descDRTestObjective, descDRTestTags,
14
+ descInputsetIdentity, descRuntimeInputs, descHubIdentity, descInfraType, descExperimentName, descExperimentIdentity, descInfraRef, descExperimentId, descInfraStatus, descLoadtestName, descLoadtestType, descLoadtestIdentity, descLoadtestDescription, descLoadtestTags, descLoadtestEnvId, descLoadtestInfraId, descLoadtestTargetType, descLoadtestTargetUrl, descLoadtestScript, descLoadtestUsers, descLoadtestDurationSec, descLoadtestRampUpSec, descLoadtestWorkerCount, descLoadtestScriptSource, descLoadtestScriptImage, descLoadtestScriptEntrypoint, descLoadtestLoadArgs, descLoadtestImagePullSecret, descLoadtestHostUrl, descLoadtestRpsLimit, descLoadtestIterations, descLoadtestEnvVars, descLoadtestProperties, descLoadtestThresholds, descLoadtestCleanupPolicy, descLoadtestResources, descHubIdentityExact, descHubName, descHubNameUpdate, descHubDescription, descHubDescriptionUpdate, descHubTags, descHubTagsReplace, descConnectorRef, descRepoName, descRepoBranch, descHubSearch, descIncludeAllScope, descTemplateSearch, descSortField, descSortAsc, descTags, descInfrastructure, descTemplateIdentity, descRevision, descRevision1, descRevision2, descRevisionToCompare, descFaultType, descFaultCategory, descFaultPermissions, descFaultIsEnterprise, descImportType, descExperimentDescription, descExperimentTags, descEntityTypeProbe, descEntityTypeAction, descEntityTypeFault, descPermissionsRequiredEnum, descOnlyTemplatisedFaults, descEnvironmentId, descK8sInfraStatus, descIncludeLegacyInfra, descSearchK8sInfra, descChaosEnabledInfraType, descInfraScope, descInfraAiEnabled, descSearchTermEnv, descSortEnv, descEnvironmentType, descGuardSearch, descGuardInfraType, descGuardTags, descGuardEnabled, descExperimentRunIdStop, descNotifyId, descForce, descIsEnabledFlag, descIsBulkUpdate, descVerifyFlag, descExperimentRunIds, descNotifyIds, descFaultIdentityParam, descIsEnterpriseFilter, descFaultSearch, descFaultListType, descFaultListInfraType, descFaultListInfrastructure, descFaultListTags, descFaultListCategory, descFaultListSortField, descFaultListSortAscending, descIsEnterpriseYaml, descIsEnterpriseVars, descIsEnterpriseRuns, descActionIdentityParam, descSearchActionsParam, descHubIdentityActions, descExperimentVariablesParam, descTasksParam, descEnvironmentIdCreate, descInfraIdCreate, descAccountIdBody, descOrgIdBody, descProjectIdBody, descSearchExperiments, descExperimentInfraId, descExperimentEnvironmentId, descExperimentIds, descExperimentStartDate, descExperimentEndDate, descExperimentTargetNetworkMapIds, descExperimentMyExperiments, descExperimentExcludeAutomation, descSearchProbes, descProbeIds, descProbeSortField, descProbeIdField, descProbeNameField, descProbePropertiesField, descRunPropertiesField, descDRTestSort, descCreateDRTest, descBodyDRTestCreate, descDRTestName, descDRTestIdentifier, descDRTestDescription, descDRTestObjective, descDRTestTags,
15
15
  // Input set descriptions
16
16
  descChaosInputSet, descListInputSets, descGetInputSet, descCreateInputSet, descUpdateInputSet, descDeleteInputSet, descInputSetIdentityField, descInputSetName, descInputSetDescription, descInputSetSpec, descIsIdentity,
17
17
  // Component variable descriptions
@@ -19,7 +19,7 @@ descComponentType, descComponentIdentifier, descComponentHubReference,
19
19
  // Experiment create field descriptions
20
20
  descExperimentManifest, descExperimentInfraType, descExperimentInfraIdCreate, descExperimentCronSyntax, descExperimentIdUUID,
21
21
  // Service Discovery
22
- descSDAgentIdentity, descSDEnvironmentId, descSDFetchAll, descSDAgentDiagnostic, descDiscoveredNamespace, descListDiscoveredNamespaces, descSDNamespaceNameFilter, descDiscoveredService, descListDiscoveredServices, descSDNamespaceFilter, descSDSearchFilter,
22
+ descSDAgentIdentity, descSDEnvironmentId, descSDAgentListEnvironmentId, descSDFetchAll, descSDAgentDiagnostic, descDiscoveredAgent, descListDiscoveredAgents, descDiscoveredAgentSearch, descDiscoveredNamespace, descListDiscoveredNamespaces, descSDNamespaceNameFilter, descDiscoveredService, descListDiscoveredServices, descSDNamespaceFilter, descSDSearchFilter,
23
23
  // Scanned Risks / Risk Rules / Risk Scans (chaos-manager v3)
24
24
  descScannedRisk, descChaosRiskRule, descChaosRiskScan, descListScannedRisks, descGetScannedRisk, descListScannedRiskOccurrences, descSummarizeScannedRisksByService, descListRiskRules, descGetRiskRule, descListRiskScans, descGetRiskScan, descCreateRiskScan, descUpdateRiskScan, descDeleteRiskScan, descRetryRiskScan, descAbortRiskScan, descGetRiskScanReport, descGetRiskScanReportDownload, descGetRiskScanHeatmap, descBodyRiskScanCreate, descBodyRiskScanUpdate, descRiskScanIdentity, descRiskScanName, descRiskScanDescription, descRiskScanTags, descRiskScanType, descRiskScanSource, descScannedRiskSeverity, descScannedRiskRuleId, descScannedRiskValidationType, descScannedRiskServiceIdentity, descScannedRiskScanType, descRiskEnvironmentIdentity, descRiskAgentIdentity, descRiskPipelineIdentity, descRiskScanStatus, descRiskSearch, descRiskSortField, descRiskSortAscending, descRiskTags, descRiskIncludeAllScope, descRiskStartTime, descRiskEndTime, descRiskRuleDataSource, descRiskRuleIsSystem, descHeatmapSearch, } from "./chaos-descriptions.js";
25
25
  /**
@@ -44,6 +44,39 @@ const chaosComponentVarExtract = (raw) => {
44
44
  const r = raw;
45
45
  return r.items?.[0] ?? raw;
46
46
  };
47
+ /** Compact projection for chaos_loadtest list items — keeps cheap discriminator
48
+ * scalars (toolType/targetType/scriptSource/infraType/cleanupPolicy) that the
49
+ * generic compactItems() whitelist in utils/compact.ts would otherwise drop
50
+ * (it only matches the literal key "type", not "toolType"/"targetType"), while
51
+ * still excluding heavy fields (toolConfig, yaml, variables, envVars, recentRuns). */
52
+ function compactLoadTest(item) {
53
+ const slim = {};
54
+ for (const key of [
55
+ "loadtestId", "uniqueId", "identity", "name", "description", "tags",
56
+ "environmentIdentifier", "infraIdentifier", "infraType", "targetType",
57
+ "toolType", "scriptSource", "cleanupPolicy", "latestRevisionIdentifier",
58
+ "createdAt", "updatedAt", "openInHarness",
59
+ ]) {
60
+ if (item[key] !== undefined)
61
+ slim[key] = item[key];
62
+ }
63
+ return slim;
64
+ }
65
+ /** Compact projection for discovered_agent list items — keeps serviceCount/
66
+ * networkMapCount (STEP 1 of chaos_service create shows these) which the
67
+ * generic compactItems() whitelist in utils/compact.ts would otherwise drop. */
68
+ function compactDiscoveredAgent(item) {
69
+ const slim = {};
70
+ for (const key of [
71
+ "identity", "name", "description", "tags", "environmentIdentifier",
72
+ "serviceCount", "networkMapCount", "installationType",
73
+ "createdAt", "updatedAt", "openInHarness",
74
+ ]) {
75
+ if (item[key] !== undefined)
76
+ slim[key] = item[key];
77
+ }
78
+ return slim;
79
+ }
47
80
  /**
48
81
  * Parse input.body when LLMs double-serialize it as a JSON string instead of an object.
49
82
  * Fails loudly on malformed JSON so callers' defaults can never silently produce a
@@ -62,56 +95,19 @@ function coerceBody(input) {
62
95
  }
63
96
  return raw;
64
97
  }
65
- /**
66
- * Map ergonomic snake_case scalars to the canonical TemplateInput[] array the
67
- * load-test backend expects. Only emits entries for values the caller provided
68
- * (plus the three always-present run params and the K8s-only workerCount).
69
- */
70
- function buildLoadtestInputs(b, opts) {
71
- const inputs = [];
72
- // Run params: always emitted (backend treats targetUsers as required).
73
- const users = b.users != null ? b.users : 100;
74
- const duration = b.duration_sec != null ? b.duration_sec : 600;
75
- const rampUp = b.ramp_up_sec != null ? b.ramp_up_sec : 120;
76
- inputs.push({ name: "targetUsers", value: users, type: "Integer", required: true });
77
- inputs.push({ name: "durationSeconds", value: duration, type: "Integer" });
78
- inputs.push({ name: "rampUpTimeSec", value: rampUp, type: "Integer" });
79
- // workerCount is Kubernetes-only (0 = standalone, N > 0 = distributed).
80
- if (opts.targetType === "kubernetes") {
81
- const workerCount = b.worker_count != null ? b.worker_count : 0;
82
- inputs.push({ name: "workerCount", value: workerCount, type: "Integer" });
83
- }
84
- // Target URL: present on every load test (backend treats it as the host under test).
85
- const targetUrl = (b.target_url ?? b.targetUrl);
86
- if (targetUrl != null) {
87
- inputs.push({ name: "targetUrl", value: targetUrl, type: "String" });
88
- }
89
- // Image-mode tunables (Custom Image): scriptImage/scriptEntrypoint are
90
- // mandatory in image mode (marked required), loadArgs is optional.
91
- if (opts.scriptSource === "image") {
92
- const scriptImage = (b.script_image ?? b.scriptImage);
93
- if (scriptImage != null) {
94
- inputs.push({ name: "scriptImage", value: scriptImage, type: "String", required: true });
95
- }
96
- const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
97
- if (entrypoint != null) {
98
- inputs.push({ name: "scriptEntrypoint", value: entrypoint, type: "String", required: true });
99
- }
100
- const loadArgs = (b.load_args ?? b.loadArgs);
101
- if (loadArgs != null) {
102
- inputs.push({ name: "loadArgs", value: loadArgs, type: "String" });
103
- }
104
- }
105
- return inputs;
106
- }
107
- // ── K6-specific helpers ──────────────────────────────────────────────
108
- // K6 is a Kubernetes-only load test runner. Script mode wraps the K6 JS source
109
- // in a `toolConfig` object (rather than top-level `scriptContent` like Locust).
110
- // Env vars and secret references live exclusively in `toolConfig.envVars`.
111
- // Reserved K6 env-var names (case-insensitive). Mirrors
112
- // loadTestManager/internal/domain/ReservedEnvVarNames; the frontend list is at
98
+ // ── Load test helpers ────────────────────────────────────────────────
99
+ // Since the loadTestManager variables migration, all tunables and custom env
100
+ // vars live under toolConfig.<tool>.tunables / toolConfig.<tool>.variables. The
101
+ // MCP keeps its LLM surface as ergonomic snake_case scalars and translates
102
+ // them into the nested toolConfig shape here, so agents never construct the
103
+ // nested map, base64, or YAML themselves.
104
+ // ── Shared env-var helper (K6 + JMeter) ──────────────────────────────
105
+ // Env vars and secret references live in `toolConfig.<tool>.envVars`.
106
+ // Reserved env-var names (case-insensitive). Mirrors
107
+ // loadTestManager/internal/domain/ReservedEnvVarNames (shared base set, all
108
+ // tools) + K6ReservedEnvVarNames (K6-only extras). The frontend list is at
113
109
  // hce-saas/web/src/services/loadTest/loadTestVariables.ts:337-372.
114
- const RESERVED_K6_ENV_VAR_NAMES = new Set([
110
+ const BASE_RESERVED_ENV_VAR_NAMES = new Set([
115
111
  "RUN_ID", "LOAD_TEST_ID", "TARGET_USERS", "SPAWN_RATE", "SCRIPT_CONTENT_BASE64",
116
112
  "TARGET_URL", "ACCOUNT_ID", "ORG_ID", "PROJECT_ID", "ENV_ID", "DURATION_SECONDS",
117
113
  "CONTROL_PLANE_URL", "CONTROL_PLANE_TOKEN", "HARNESS_CUSTOM_VAR_NAMES",
@@ -119,18 +115,21 @@ const RESERVED_K6_ENV_VAR_NAMES = new Set([
119
115
  "PYTHONPATH", "PATH", "HOME", "USER", "SHELL", "LANG", "TERM", "HOSTNAME",
120
116
  "PWD", "LD_LIBRARY_PATH", "LD_PRELOAD", "TMPDIR", "TMP", "TEMP",
121
117
  ]);
122
- const K6_ENV_VAR_KEY_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
118
+ const K6_EXTRA_RESERVED_ENV_VAR_NAMES = new Set([
119
+ "HOST_URL", "K6_VUS", "K6_DURATION", "K6_ITERATIONS", "K6_STAGES", "K6_RPS",
120
+ ]);
121
+ const ENV_VAR_KEY_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
123
122
  // Build the HSM secret reference: account → "account.<id>", org → "org.<id>", project → "<id>".
124
123
  // Matches the Harness UI convention (scope prefix encodes which secret manager the value lives in).
125
124
  function buildSecretReference(secretId, scope) {
126
125
  const prefix = scope === "account" ? "account." : scope === "org" ? "org." : "";
127
126
  return `secrets.getValue("${prefix}${secretId}")`;
128
127
  }
129
- // Validate + project the structured env_vars input into the wire shape K6 expects.
128
+ // Validate + project the structured env_vars input into the wire shape K6/JMeter expect.
130
129
  // Each entry sets exactly one of:
131
130
  // - { key, value } → literal env var
132
131
  // - { key, secret_id, secret_scope?: "..." } → MCP builds the secrets.getValue(...) string
133
- function buildK6EnvVars(raw) {
132
+ function buildEnvVars(raw, tool) {
134
133
  if (raw == null)
135
134
  return [];
136
135
  if (!Array.isArray(raw)) {
@@ -143,10 +142,11 @@ function buildK6EnvVars(raw) {
143
142
  if (!key || typeof key !== "string") {
144
143
  throw new Error(`env_vars[${idx}].key is required.`);
145
144
  }
146
- if (!K6_ENV_VAR_KEY_REGEX.test(key)) {
145
+ if (!ENV_VAR_KEY_REGEX.test(key)) {
147
146
  throw new Error(`env_vars[${idx}].key '${key}' is invalid: must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
148
147
  }
149
- if (RESERVED_K6_ENV_VAR_NAMES.has(key.toUpperCase())) {
148
+ const upper = key.toUpperCase();
149
+ if (BASE_RESERVED_ENV_VAR_NAMES.has(upper) || (tool === "K6" && K6_EXTRA_RESERVED_ENV_VAR_NAMES.has(upper))) {
150
150
  throw new Error(`env_vars[${idx}].key '${key}' is a reserved name and cannot be used as a custom env var.`);
151
151
  }
152
152
  if (seen.has(key)) {
@@ -181,108 +181,319 @@ function parseHostOrigin(rawUrl) {
181
181
  return rawUrl;
182
182
  }
183
183
  }
184
- // Build the K6 `toolConfig` object for script or image mode (UI mode is deferred).
185
- // Script mode: enforces the mandatory `export default function` rule client-side so the
186
- // agent gets a clear error before the API trip. Image mode: wires the prebuilt container
187
- // image (+ optional entrypoint) into customImage. hostUrl / options / envVars are shared.
184
+ // Build the K6 `toolConfig.k6` block for script or image mode (UI mode deferred).
185
+ // Matches K6Spec + ScriptSpec in loadTestManager: mode +
186
+ // script{content|image|entrypoint|loadArgs|imagePullSecret} +
187
+ // tunables{targetUrl, targetUsers, durationSeconds, rampUpTimeSec, workerCount, hostUrl,
188
+ // iterations, rpsLimit} + envVars.
188
189
  function buildK6ToolConfig(b, args) {
189
190
  const hostUrl = b.host_url ??
190
191
  b.hostUrl ??
191
192
  parseHostOrigin(args.targetUrl);
192
193
  const rpsLimit = b.rps_limit != null ? b.rps_limit : undefined;
193
- const envVars = buildK6EnvVars(b.env_vars);
194
- const options = {};
195
- if (rpsLimit != null && rpsLimit > 0)
196
- options.rpsLimit = rpsLimit;
197
- // Build the mode-specific payload first; shared keys (hostUrl, options, envVars,
198
- // iterations) are appended after so we get a stable key order matching the studio.
199
- let toolConfig;
194
+ const iterations = b.iterations != null ? b.iterations : undefined;
195
+ const envVars = buildEnvVars(b.env_vars, "K6");
196
+ // Script/image artifact lives under `script`, not `scriptContent`/`customImage`.
197
+ let mode;
198
+ const script = {};
200
199
  if (args.scriptSource === "image") {
201
- // Custom Image: image + optional entrypoint live in toolConfig.customImage.
202
- // Note: load_args is NOT part of customImage for K6 — it rides only in inputs[]
203
- // (matches K6LoadTestService.ts:487-495 where customImage only has image/entrypoint).
204
200
  const scriptImage = (b.script_image ?? b.scriptImage);
205
201
  if (scriptImage == null) {
206
- // Defensive — the early image-mode gate in bodyBuilder already throws this.
207
202
  throw new Error("K6 image mode requires 'script_image'.");
208
203
  }
209
- const customImage = { image: scriptImage };
204
+ mode = "image";
205
+ script.image = scriptImage;
210
206
  const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
211
207
  if (entrypoint != null)
212
- customImage.entrypoint = entrypoint;
213
- toolConfig = { mode: "image", customImage };
208
+ script.entrypoint = entrypoint;
209
+ const loadArgs = (b.load_args ?? b.loadArgs);
210
+ if (loadArgs != null && loadArgs !== "") {
211
+ validateLoadArgs(loadArgs);
212
+ script.loadArgs = loadArgs;
213
+ }
214
+ const imagePullSecret = (b.image_pull_secret ?? b.imagePullSecret);
215
+ if (imagePullSecret != null && imagePullSecret !== "") {
216
+ script.imagePullSecret = imagePullSecret;
217
+ }
214
218
  }
215
219
  else {
216
- // Script mode: base64 JS source. Mandatory client-side rule (matches Harness UI:
217
- // validateScriptContent at K6LoadTestService.ts:767-774). Substring check (not
218
- // full AST) matches the UI exactly.
219
220
  if (args.script == null) {
220
221
  throw new Error("K6 script mode requires 'script' (the raw JavaScript K6 source).");
221
222
  }
223
+ // Mandatory client-side rule (matches Harness UI validateScriptContent).
222
224
  if (!args.script.includes("export default")) {
223
225
  throw new Error("K6 script must export a default function (export default function ...).");
224
226
  }
225
- toolConfig = {
226
- mode: "script",
227
- scriptContent: Buffer.from(args.script, "utf8").toString("base64"),
228
- };
229
- // iterations is K6 script-mode-only (matches K6LoadTestService.ts:483-484).
230
- const iterations = b.iterations != null ? b.iterations : undefined;
231
- if (iterations != null && iterations > 0)
232
- toolConfig.iterations = iterations;
227
+ mode = "script";
228
+ script.content = Buffer.from(args.script, "utf8").toString("base64");
233
229
  }
230
+ // Tunables live under `tunables`, not flat on the toolConfig object.
231
+ const tunables = {};
232
+ if (args.targetUrl != null)
233
+ tunables.targetUrl = args.targetUrl;
234
+ if (b.users != null)
235
+ tunables.targetUsers = b.users;
236
+ if (b.duration_sec != null)
237
+ tunables.durationSeconds = b.duration_sec;
238
+ if (b.ramp_up_sec != null)
239
+ tunables.rampUpTimeSec = b.ramp_up_sec;
240
+ if (b.worker_count != null)
241
+ tunables.workerCount = b.worker_count;
234
242
  if (hostUrl)
235
- toolConfig.hostUrl = hostUrl;
236
- if (Object.keys(options).length > 0)
237
- toolConfig.options = options;
243
+ tunables.hostUrl = hostUrl;
244
+ if (iterations != null && iterations > 0)
245
+ tunables.iterations = iterations;
246
+ // rpsLimit's sole authoring surface is tunables (loadTestManager k6.go);
247
+ // options.rpsLimit is a legacy run-dispatch fallback only, never written here.
248
+ if (rpsLimit != null && rpsLimit > 0)
249
+ tunables.rpsLimit = rpsLimit;
250
+ const toolConfig = { mode, script };
251
+ if (Object.keys(tunables).length > 0)
252
+ toolConfig.tunables = tunables;
238
253
  if (envVars.length > 0)
239
254
  toolConfig.envVars = envVars;
240
255
  return toolConfig;
241
256
  }
257
+ // Reject known legacy/mistaken field names on chaos_loadtest create/update instead
258
+ // of silently ignoring them (Fail Loudly — see coerceBody above). 'scriptContent' /
259
+ // 'customImage' are old wire-shape names superseded by 'script' / 'script_image';
260
+ // 'inputs' belongs to chaos_probe, not chaos_loadtest.
261
+ const LEGACY_LOADTEST_KEYS = ["scriptContent", "customImage", "inputs"];
262
+ function rejectLegacyLoadtestFields(b) {
263
+ for (const key of LEGACY_LOADTEST_KEYS) {
264
+ if (b[key] !== undefined) {
265
+ throw new Error(`'${key}' is not a supported chaos_loadtest field. Use 'script'/'script_image' instead of 'scriptContent'/'customImage'; 'inputs' is a chaos_probe field, not chaos_loadtest.`);
266
+ }
267
+ }
268
+ }
269
+ const JMETER_THRESHOLD_METRICS = new Set(["response_time_ms", "error_rate_pct", "throughput_rps", "latency_ms"]);
270
+ const JMETER_THRESHOLD_OPERATORS = new Set(["<", "<=", ">", ">=", "==", "!="]);
271
+ const JMETER_STAT_REQUIRED_METRICS = new Set(["response_time_ms", "latency_ms"]);
272
+ // Validate + project the structured properties input into toolConfig.jmeter.properties[].
273
+ function buildJMeterProperties(raw) {
274
+ if (raw == null)
275
+ return [];
276
+ if (!Array.isArray(raw)) {
277
+ throw new Error("properties must be an array of {key, value, send_to_engines?} entries.");
278
+ }
279
+ return raw.map((entry, idx) => {
280
+ const p = entry;
281
+ const key = p.key;
282
+ if (!key) {
283
+ throw new Error(`properties[${idx}].key is required.`);
284
+ }
285
+ const out = { key, value: String(p.value ?? "") };
286
+ if (typeof p.send_to_engines === "boolean")
287
+ out.sendToEngines = p.send_to_engines;
288
+ return out;
289
+ });
290
+ }
291
+ // Validate + project the structured thresholds input into toolConfig.jmeter.thresholds[].
292
+ // Mirrors JMeterThreshold.validate() in loadTestManager/internal/domain/jmeter.go:245-256.
293
+ function buildJMeterThresholds(raw) {
294
+ if (raw == null)
295
+ return [];
296
+ if (!Array.isArray(raw)) {
297
+ throw new Error("thresholds must be an array of {metric, stat?, operator, value, abort_on_fail?} entries.");
298
+ }
299
+ return raw.map((entry, idx) => {
300
+ const t = entry;
301
+ const metric = t.metric;
302
+ if (!metric || !JMETER_THRESHOLD_METRICS.has(metric)) {
303
+ throw new Error(`thresholds[${idx}].metric must be one of ${[...JMETER_THRESHOLD_METRICS].join(", ")}.`);
304
+ }
305
+ const operator = t.operator;
306
+ if (!operator || !JMETER_THRESHOLD_OPERATORS.has(operator)) {
307
+ throw new Error(`thresholds[${idx}].operator must be one of ${[...JMETER_THRESHOLD_OPERATORS].join(", ")}.`);
308
+ }
309
+ const stat = t.stat;
310
+ if (JMETER_STAT_REQUIRED_METRICS.has(metric) && !stat) {
311
+ throw new Error(`thresholds[${idx}]: metric '${metric}' requires 'stat' (e.g. p95, p99, avg, median, max).`);
312
+ }
313
+ if (t.value == null) {
314
+ throw new Error(`thresholds[${idx}].value is required.`);
315
+ }
316
+ const out = { metric, operator, value: t.value };
317
+ if (stat)
318
+ out.stat = stat;
319
+ if (typeof t.abort_on_fail === "boolean")
320
+ out.abortOnFail = t.abort_on_fail;
321
+ return out;
322
+ });
323
+ }
324
+ // Build the JMeter `toolConfig.jmeter` block for script or image mode.
325
+ // Matches JMeterSpec in loadTestManager/internal/domain/jmeter.go: mode + script{content|image|entrypoint}
326
+ // + tunables{workerCount} + properties[] + envVars[] + thresholds[].
327
+ function buildJMeterToolConfig(b, args) {
328
+ let mode;
329
+ const script = {};
330
+ if (args.scriptSource === "image") {
331
+ const scriptImage = (b.script_image ?? b.scriptImage);
332
+ if (scriptImage == null) {
333
+ throw new Error("JMeter image mode requires 'script_image'.");
334
+ }
335
+ mode = "image";
336
+ script.image = scriptImage;
337
+ const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
338
+ if (entrypoint != null)
339
+ script.entrypoint = entrypoint;
340
+ const loadArgs = (b.load_args ?? b.loadArgs);
341
+ if (loadArgs != null && loadArgs !== "") {
342
+ validateLoadArgs(loadArgs);
343
+ script.loadArgs = loadArgs;
344
+ }
345
+ const imagePullSecret = (b.image_pull_secret ?? b.imagePullSecret);
346
+ if (imagePullSecret != null && imagePullSecret !== "") {
347
+ script.imagePullSecret = imagePullSecret;
348
+ }
349
+ }
350
+ else {
351
+ if (args.script == null) {
352
+ throw new Error("JMeter script mode requires 'script' (the raw .jmx/.xml plan text).");
353
+ }
354
+ mode = "script";
355
+ script.content = Buffer.from(args.script, "utf8").toString("base64");
356
+ }
357
+ const toolConfig = { mode, script };
358
+ if (b.worker_count != null)
359
+ toolConfig.tunables = { workerCount: b.worker_count };
360
+ const properties = buildJMeterProperties(b.properties);
361
+ if (properties.length > 0)
362
+ toolConfig.properties = properties;
363
+ const envVars = buildEnvVars(b.env_vars, "JMeter");
364
+ if (envVars.length > 0)
365
+ toolConfig.envVars = envVars;
366
+ const thresholds = buildJMeterThresholds(b.thresholds);
367
+ if (thresholds.length > 0)
368
+ toolConfig.thresholds = thresholds;
369
+ return toolConfig;
370
+ }
371
+ // Validate the Locust/K6/JMeter load_args string. Mirrors ValidateLoadArgs in
372
+ // loadTestManager/internal/api/dto.go: semicolon-separated k=v pairs; keys
373
+ // must be non-empty, contain no whitespace, and not start with '-'.
374
+ function validateLoadArgs(loadArgs) {
375
+ for (const raw of loadArgs.split(";")) {
376
+ const pair = raw.trim();
377
+ if (!pair)
378
+ continue;
379
+ const eq = pair.indexOf("=");
380
+ const key = (eq >= 0 ? pair.slice(0, eq) : pair).trim();
381
+ if (!key)
382
+ throw new Error(`load_args: empty key in pair ${JSON.stringify(pair)}`);
383
+ if (/[ \t]/.test(key)) {
384
+ throw new Error(`load_args: key ${JSON.stringify(key)} must not contain spaces`);
385
+ }
386
+ if (key.startsWith("-")) {
387
+ throw new Error(`load_args: key ${JSON.stringify(key)} must not start with '-'`);
388
+ }
389
+ }
390
+ }
391
+ // Build the Locust `toolConfig.locust` block for script or image mode. Matches
392
+ // LocustSpec + ScriptSpec in loadTestManager: mode +
393
+ // script{content|image|entrypoint|loadArgs|imagePullSecret} +
394
+ // tunables{targetUrl, targetUsers, spawnRate, rampUpTimeSec, durationSeconds, workerCount}.
395
+ // NOTE: Locust intentionally has no target_type guard (unlike K6/JMeter) —
396
+ // loadTestManager supports Locust script AND image mode on both Linux VM and
397
+ // Kubernetes infra (loadtest_handlers.go has no Locust kubernetes-only check).
398
+ // Do not add one here.
399
+ function buildLocustToolConfig(b, args) {
400
+ let mode;
401
+ const script = {};
402
+ if (args.scriptSource === "image") {
403
+ const scriptImage = (b.script_image ?? b.scriptImage);
404
+ if (scriptImage == null) {
405
+ throw new Error("Locust image mode requires 'script_image'.");
406
+ }
407
+ mode = "image";
408
+ script.image = scriptImage;
409
+ const entrypoint = (b.script_entrypoint ?? b.scriptEntrypoint);
410
+ if (entrypoint != null)
411
+ script.entrypoint = entrypoint;
412
+ const loadArgs = (b.load_args ?? b.loadArgs);
413
+ if (loadArgs != null && loadArgs !== "") {
414
+ validateLoadArgs(loadArgs);
415
+ script.loadArgs = loadArgs;
416
+ }
417
+ const imagePullSecret = (b.image_pull_secret ?? b.imagePullSecret);
418
+ if (imagePullSecret != null && imagePullSecret !== "") {
419
+ script.imagePullSecret = imagePullSecret;
420
+ }
421
+ }
422
+ else {
423
+ if (args.script == null) {
424
+ throw new Error("Locust script mode requires 'script' (the raw Python locustfile).");
425
+ }
426
+ mode = "script";
427
+ script.content = Buffer.from(args.script, "utf8").toString("base64");
428
+ }
429
+ const tunables = {};
430
+ if (args.targetUrl != null)
431
+ tunables.targetUrl = args.targetUrl;
432
+ if (b.users != null)
433
+ tunables.targetUsers = b.users;
434
+ if (b.spawn_rate != null)
435
+ tunables.spawnRate = b.spawn_rate;
436
+ if (b.duration_sec != null)
437
+ tunables.durationSeconds = b.duration_sec;
438
+ if (b.ramp_up_sec != null)
439
+ tunables.rampUpTimeSec = b.ramp_up_sec;
440
+ if (b.worker_count != null)
441
+ tunables.workerCount = b.worker_count;
442
+ const toolConfig = { mode, script };
443
+ if (Object.keys(tunables).length > 0)
444
+ toolConfig.tunables = tunables;
445
+ return toolConfig;
446
+ }
242
447
  /**
243
448
  * Build the canonical LoadTest YAML manifest. Mirrors the studio's
244
449
  * formDataToManifest shape (kind: LoadTest, apiVersion: v1alpha1, spec.{...}).
245
450
  *
246
- * scriptContent inside the YAML is PLAIN TEXT (so the manifest is human-
247
- * readable); only the JSON request body's top-level scriptContent is base64.
248
- * Caller base64-encodes the returned string into the `yaml` request field.
451
+ * The wire toolBlock carries base64 script.content; for readability the YAML
452
+ * view carries plain-text script.content instead. Caller base64-encodes the
453
+ * returned string into the `yaml` request field.
249
454
  */
250
455
  function buildLoadtestYamlManifest(args) {
251
456
  const infraType = args.targetType === "kubernetes" ? "kubernetes" : "linux";
457
+ const metadata = { name: args.name };
458
+ if (args.description)
459
+ metadata.description = args.description;
460
+ if (args.tags && args.tags.length)
461
+ metadata.tags = args.tags;
462
+ if (args.serviceReferences && args.serviceReferences.length) {
463
+ metadata.serviceReferences = args.serviceReferences;
464
+ }
252
465
  const manifest = {
253
466
  kind: "LoadTest",
254
467
  apiVersion: "v1alpha1",
255
- name: args.name,
468
+ metadata,
256
469
  };
257
- if (args.description)
258
- manifest.description = args.description;
259
- if (args.tags && args.tags.length)
260
- manifest.tags = args.tags;
470
+ // In the YAML view we want plain-text script content for readability; the
471
+ // wire toolConfig carries it base64. Decode a copy for the manifest.
472
+ const yamlToolBlock = { ...args.toolBlock };
473
+ const s = yamlToolBlock.script;
474
+ if (s?.content && typeof s.content === "string") {
475
+ try {
476
+ yamlToolBlock.script = {
477
+ ...s,
478
+ content: Buffer.from(s.content, "base64").toString("utf8"),
479
+ };
480
+ }
481
+ catch {
482
+ // If the content isn't valid base64 for any reason, keep as-is.
483
+ }
484
+ }
261
485
  const spec = {
262
486
  identity: args.identity,
263
487
  toolType: args.toolType,
264
488
  infraType,
265
489
  targetType: args.targetType,
266
- scriptSource: args.scriptSource,
490
+ cleanupPolicy: args.cleanupPolicy ?? "delete",
267
491
  };
268
- // Locust inline only: readable scriptContent at the manifest root (plain text).
269
- // K6 keeps the script inside spec.toolConfig instead.
270
- if (args.toolType === "Locust" && args.scriptSource === "inline" && args.script) {
271
- spec.scriptContent = args.script;
272
- }
492
+ if (args.resources != null)
493
+ spec.resources = args.resources;
273
494
  spec.infraId = args.infraIdentifier;
274
495
  spec.envId = args.environmentIdentifier;
275
- spec.inputs = args.inputs;
276
- // K6: emit spec.toolConfig with PLAIN TEXT scriptContent (decode the base64 from
277
- // the wire-shape toolConfig so the YAML view is human-readable). Other
278
- // toolConfig keys (mode, hostUrl, options, envVars, iterations) pass through.
279
- if (args.toolType === "K6" && args.k6ToolConfig) {
280
- const tc = { ...args.k6ToolConfig };
281
- if (typeof tc.scriptContent === "string" && args.script) {
282
- tc.scriptContent = args.script; // plain text for YAML readability
283
- }
284
- spec.toolConfig = tc;
285
- }
496
+ spec.toolConfig = { [args.toolType.toLowerCase()]: yamlToolBlock };
286
497
  manifest.spec = spec;
287
498
  return YAML.stringify(manifest);
288
499
  }
@@ -1516,7 +1727,34 @@ export const chaosToolset = {
1516
1727
  scope: "project",
1517
1728
  scopeParams: CHAOS_SCOPE,
1518
1729
  identifierFields: ["loadtest_id"],
1730
+ compactItem: compactLoadTest,
1519
1731
  deepLinkTemplate: "/ng/account/{accountId}/module/chaos/orgs/{orgIdentifier}/projects/{projectIdentifier}/load-tests/{loadtestId}",
1732
+ listFilterFields: [
1733
+ { name: "environment_id", description: descEnvironmentId },
1734
+ { name: "tool_type", description: descLoadtestType, enum: ["Locust", "K6", "JMeter"] },
1735
+ { name: "tags", description: descLoadtestTags },
1736
+ { name: "search", description: "Free-text search. NOTE: backend currently matches only the load test's display name (substring, case-insensitive) -- it does not match identity/slug and is not fuzzy or prefix-tolerant." },
1737
+ { name: "sort_field", description: "Field to sort by (e.g. createdAt, updatedAt, name)." },
1738
+ { name: "sort_ascending", description: "Sort ascending when true, descending when false.", type: "boolean" },
1739
+ ],
1740
+ diagnosticHint: "Load tests are looked up by their identity slug (e.g. 'mcplocustscript001'), not the uniqueId UUID. If you only have a uniqueId, call harness_list resource_type=chaos_loadtest (optionally with search_term) and use the matching item's 'identity'/'loadtestId' field instead.",
1741
+ relatedResources: [
1742
+ {
1743
+ resourceType: "chaos_infrastructure",
1744
+ relationship: "prerequisite",
1745
+ description: "Linux VM load-runner infrastructure. When the load test targets Linux VM (target_type='machine-chaos-linux', the default), its 'environmentID' supplies environment_id and its 'infraID' supplies infra_id. Only loadEnabled + ACTIVE infras are usable. In MCP, Linux VM supports tool_type Locust only -- K6 (a genuine backend restriction) and JMeter (an MCP/UI-parity restriction; the backend itself allows Linux JMeter) with a Linux target_type are rejected by MCP.",
1746
+ },
1747
+ {
1748
+ resourceType: "chaos_enabled_infrastructure",
1749
+ relationship: "prerequisite",
1750
+ description: "Kubernetes load-runner infrastructure. When the load test targets Kubernetes, filter this list by infra_type='KubernetesV2'; its 'environmentID' supplies environment_id and its 'infraID' supplies infra_id. Kubernetes infra supports tool_type Locust, K6, or JMeter. IMPORTANT: target_type MUST be explicitly set to 'kubernetes' -- it is not auto-derived from which infra list you picked from; omitting it defaults to Linux naming and dispatches wrong.",
1751
+ },
1752
+ {
1753
+ resourceType: "chaos_service",
1754
+ relationship: "prerequisite",
1755
+ description: "Resilience Testing Services attached to a load test. List with infrastructure_ids='<environment_id>/<infra_id>' to see services already onboarded on the chosen load-runner infra. A user may pick one or more identities from the list, OR onboard a brand-new service via chaos_service's create flow (see chaos_service.relatedResources for the discovered_agent/discovered_service chain) regardless of whether the list is empty -- creating a new service is a user-intent decision, not gated on list length. The chosen identity/identities are passed on the load test as service_references (required when the account has CHAOS_RISK_SERVICES_ENABLED on).",
1756
+ },
1757
+ ],
1520
1758
  operations: {
1521
1759
  list: {
1522
1760
  method: "GET",
@@ -1529,6 +1767,10 @@ export const chaosToolset = {
1529
1767
  search: "search",
1530
1768
  search_term: "search",
1531
1769
  environment_id: "environmentIdentifier",
1770
+ tool_type: "toolType",
1771
+ tags: "tags",
1772
+ sort_field: "sortField",
1773
+ sort_ascending: "sortAscending",
1532
1774
  },
1533
1775
  responseExtractor: chaosLoadTestListExtract,
1534
1776
  description: descListLoadtests,
@@ -1548,45 +1790,33 @@ export const chaosToolset = {
1548
1790
  skipScopeBodyInjection: true,
1549
1791
  bodyBuilder: (input) => {
1550
1792
  const b = coerceBody(input);
1793
+ rejectLegacyLoadtestFields(b);
1551
1794
  const name = b.name ?? "";
1552
1795
  if (!name) {
1553
1796
  throw new Error("name is required.");
1554
1797
  }
1555
1798
  // identity is the slug-constrained key; auto-derive from name when omitted.
1556
- // The display name is permissive (any non-empty string) — only the identity
1557
- // must be a slug. We strip non-alphanumerics from name as a sensible default;
1558
- // a fully-blank slug falls back to a UUID so we never write an empty id.
1559
1799
  const slug = (s) => s.replace(/[^a-zA-Z0-9]/g, "");
1560
1800
  const identity = b.identity || slug(name) || randomUUID();
1561
1801
  if (!/^[a-zA-Z0-9_]+$/.test(identity)) {
1562
1802
  throw new Error(`Invalid identity '${identity}': only letters, numbers and underscores are allowed (derived from name when omitted).`);
1563
1803
  }
1804
+ // Tool type: Locust (default), K6, or JMeter. Gatling/Custom are deprecated
1805
+ // and the backend now rejects them with a 400.
1806
+ const toolType = (b.tool_type ?? "Locust");
1807
+ if (toolType !== "Locust" && toolType !== "K6" && toolType !== "JMeter") {
1808
+ throw new Error(`tool_type '${toolType}' must be 'Locust', 'K6', or 'JMeter'.`);
1809
+ }
1564
1810
  const targetType = b.target_type ?? "machine-chaos-linux";
1811
+ if ((toolType === "K6" || toolType === "JMeter") && targetType !== "kubernetes") {
1812
+ throw new Error(`${toolType} load tests require target_type='kubernetes'.`);
1813
+ }
1565
1814
  const environmentIdentifier = (b.environment_id ?? b.environmentIdentifier);
1566
1815
  const infraIdentifier = (b.infra_id ?? b.infraIdentifier);
1567
1816
  const targetUrl = (b.target_url ?? b.targetUrl);
1568
1817
  const script = b.script;
1569
1818
  const scriptImage = (b.script_image ?? b.scriptImage);
1570
- // Explicit script_source wins, else infer "image" when an image is supplied.
1571
1819
  const scriptSource = b.script_source ?? (scriptImage != null ? "image" : "inline");
1572
- // Mode-specific required fields (fail loudly before we touch the wire).
1573
- // Tool-agnostic message: Locust = Python locust source, K6 = JavaScript with 'export default'.
1574
- // The K6-specific 'export default' rule is enforced later in buildK6ToolConfig.
1575
- if (scriptSource === "inline" && script == null) {
1576
- throw new Error("script is required when script_source='inline'.");
1577
- }
1578
- if (scriptSource === "image" && scriptImage == null) {
1579
- throw new Error("script_image is required when script_source='image'.");
1580
- }
1581
- // Tool type: Locust (default) or K6. K6 is Kubernetes-only and supports
1582
- // script and image modes (UI mode is deferred).
1583
- const toolType = (b.tool_type ?? "Locust");
1584
- if (toolType !== "Locust" && toolType !== "K6") {
1585
- throw new Error(`tool_type '${toolType}' must be 'Locust' or 'K6'.`);
1586
- }
1587
- if (toolType === "K6" && targetType !== "kubernetes") {
1588
- throw new Error("K6 load tests require target_type='kubernetes' (LinuxVM is not supported).");
1589
- }
1590
1820
  // Normalise tags (accept array or comma-separated string; default []).
1591
1821
  const rawTags = b.tags;
1592
1822
  const tags = Array.isArray(rawTags)
@@ -1594,33 +1824,28 @@ export const chaosToolset = {
1594
1824
  : typeof rawTags === "string"
1595
1825
  ? rawTags.split(",").map((t) => t.trim()).filter(Boolean)
1596
1826
  : [];
1597
- // Build the canonical inputs[] array (well-known tunables → TemplateInput[]).
1598
- // inputs[] is identical between Locust and K6 script/image modes.
1599
- const inputs = buildLoadtestInputs(b, { targetType, scriptSource });
1600
- // K6 wraps the script in a toolConfig object (no top-level scriptContent).
1601
- // Built up-front so the same value drives both the JSON body and the YAML view.
1602
- let k6ToolConfig;
1827
+ // Build toolConfig.<tool>. JMeter builds from scalars by default;
1828
+ // 'tool_config' remains an escape hatch for advanced/back-compat callers
1829
+ // (e.g. .zip bundles) who want to hand-construct the object.
1830
+ let toolBlock;
1603
1831
  if (toolType === "K6") {
1604
- k6ToolConfig = buildK6ToolConfig(b, { scriptSource, script, targetUrl });
1832
+ toolBlock = buildK6ToolConfig(b, { scriptSource, script, targetUrl });
1833
+ }
1834
+ else if (toolType === "Locust") {
1835
+ toolBlock = buildLocustToolConfig(b, { scriptSource, script, targetUrl });
1836
+ }
1837
+ else {
1838
+ const supplied = b.tool_config;
1839
+ toolBlock = supplied != null
1840
+ ? (supplied.jmeter ?? supplied)
1841
+ : buildJMeterToolConfig(b, { scriptSource, script });
1842
+ }
1843
+ const toolKey = toolType.toLowerCase();
1844
+ const toolConfig = { [toolKey]: toolBlock };
1845
+ // Variables live under toolConfig.<tool>.variables.
1846
+ if (Array.isArray(b.variables)) {
1847
+ toolBlock.variables = b.variables;
1605
1848
  }
1606
- // Build the canonical LoadTest YAML manifest, then base64-encode for the wire.
1607
- // The manifest carries plain-text scriptContent (Locust inline) OR plain-text
1608
- // toolConfig.scriptContent (K6), so the YAML view is human-readable; the JSON
1609
- // body's base64 lives only on the wire.
1610
- const yamlManifest = buildLoadtestYamlManifest({
1611
- name,
1612
- description: b.description,
1613
- tags,
1614
- identity,
1615
- toolType,
1616
- targetType,
1617
- scriptSource,
1618
- script,
1619
- k6ToolConfig,
1620
- environmentIdentifier: environmentIdentifier,
1621
- infraIdentifier: infraIdentifier,
1622
- inputs,
1623
- });
1624
1849
  const body = {
1625
1850
  identity,
1626
1851
  name,
@@ -1628,35 +1853,42 @@ export const chaosToolset = {
1628
1853
  tags,
1629
1854
  environmentIdentifier,
1630
1855
  infraIdentifier,
1631
- scriptSource,
1632
1856
  targetType,
1633
1857
  toolType,
1634
- inputs,
1635
- yaml: Buffer.from(yamlManifest, "utf8").toString("base64"),
1858
+ toolConfig,
1636
1859
  };
1637
- // Top-level scriptContent rules:
1638
- // - Locust inline: base64 of the raw Python script.
1639
- // - Locust image: omitted (script_image rides in inputs[]).
1640
- // - K6 script: OMITTED at top level — the K6 script lives in
1641
- // toolConfig.scriptContent only (top-level would be
1642
- // treated as a Locust-style script and double-encoded).
1643
- // - K6 image: not yet supported (rejected earlier).
1644
- if (toolType === "Locust" && scriptSource === "inline") {
1645
- body.scriptContent = Buffer.from(script, "utf8").toString("base64");
1646
- }
1647
- if (toolType === "K6" && k6ToolConfig) {
1648
- body.toolConfig = k6ToolConfig;
1649
- }
1650
- // Emit snake_case aliases for the registry's required-field validator (which
1651
- // checks the built body against the snake_case bodySchema field names). The
1652
- // load-test backend reads the camelCase / inputs[] keys and ignores extras
1653
- // (same pattern as chaos_action create).
1860
+ // Optional pass-through fields.
1861
+ if (b.service_references != null)
1862
+ body.serviceReferences = b.service_references;
1863
+ if (b.cleanup_policy != null)
1864
+ body.cleanupPolicy = b.cleanup_policy;
1865
+ if (b.max_duration_sec != null)
1866
+ body.maxDurationSec = b.max_duration_sec;
1867
+ if (b.resources != null)
1868
+ body.resources = b.resources;
1869
+ // Optional canonical YAML manifest (backend accepts it as an alternative
1870
+ // source; MCP still sends toolConfig authoritatively).
1871
+ const yamlManifest = buildLoadtestYamlManifest({
1872
+ name,
1873
+ description: b.description,
1874
+ tags,
1875
+ serviceReferences: body.serviceReferences,
1876
+ identity,
1877
+ toolType,
1878
+ targetType,
1879
+ toolBlock,
1880
+ environmentIdentifier: environmentIdentifier,
1881
+ infraIdentifier: infraIdentifier,
1882
+ cleanupPolicy: body.cleanupPolicy,
1883
+ resources: body.resources,
1884
+ });
1885
+ body.yaml = Buffer.from(yamlManifest, "utf8").toString("base64");
1886
+ // Snake-case aliases for the registry's required-field validator (which
1887
+ // checks the built body against the snake_case bodySchema field names).
1654
1888
  if (environmentIdentifier != null)
1655
1889
  body.environment_id = environmentIdentifier;
1656
1890
  if (infraIdentifier != null)
1657
1891
  body.infra_id = infraIdentifier;
1658
- if (targetUrl != null)
1659
- body.target_url = targetUrl;
1660
1892
  return body;
1661
1893
  },
1662
1894
  responseExtractor: chaosLoadTestExtract,
@@ -1667,18 +1899,158 @@ export const chaosToolset = {
1667
1899
  { name: "name", type: "string", required: true, description: descLoadtestName },
1668
1900
  { name: "environment_id", type: "string", required: true, description: descLoadtestEnvId },
1669
1901
  { name: "infra_id", type: "string", required: true, description: descLoadtestInfraId },
1670
- { name: "target_url", type: "string", required: true, description: descLoadtestTargetUrl },
1671
- { name: "script", type: "string", required: false, description: descLoadtestScript },
1902
+ { name: "identity", type: "string", required: false, description: descLoadtestIdentity },
1903
+ { name: "description", type: "string", required: false, description: descLoadtestDescription },
1904
+ { name: "tags", type: "array", required: false, description: descLoadtestTags },
1905
+ { name: "target_type", type: "string", required: false, description: descLoadtestTargetType },
1906
+ { name: "tool_type", type: "string", required: false, description: descLoadtestType },
1907
+ { name: "target_url", type: "string", required: false, description: descLoadtestTargetUrl },
1672
1908
  { name: "script_source", type: "string", required: false, description: descLoadtestScriptSource },
1909
+ { name: "script", type: "string", required: false, description: descLoadtestScript },
1673
1910
  { name: "script_image", type: "string", required: false, description: descLoadtestScriptImage },
1674
1911
  { name: "script_entrypoint", type: "string", required: false, description: descLoadtestScriptEntrypoint },
1675
1912
  { name: "load_args", type: "string", required: false, description: descLoadtestLoadArgs },
1676
- { name: "identity", type: "string", required: false, description: descLoadtestIdentity },
1913
+ { name: "image_pull_secret", type: "string", required: false, description: descLoadtestImagePullSecret },
1914
+ { name: "users", type: "number", required: false, description: descLoadtestUsers },
1915
+ { name: "spawn_rate", type: "number", required: false, description: "Locust spawn rate (users/sec)." },
1916
+ { name: "duration_sec", type: "number", required: false, description: descLoadtestDurationSec },
1917
+ { name: "ramp_up_sec", type: "number", required: false, description: descLoadtestRampUpSec },
1918
+ { name: "worker_count", type: "number", required: false, description: descLoadtestWorkerCount },
1919
+ { name: "host_url", type: "string", required: false, description: descLoadtestHostUrl },
1920
+ { name: "rps_limit", type: "number", required: false, description: descLoadtestRpsLimit },
1921
+ { name: "iterations", type: "number", required: false, description: descLoadtestIterations },
1922
+ { name: "env_vars", type: "array", required: false, description: descLoadtestEnvVars },
1923
+ { name: "properties", type: "array", required: false, description: descLoadtestProperties },
1924
+ { name: "thresholds", type: "array", required: false, description: descLoadtestThresholds },
1925
+ { name: "variables", type: "array", required: false, description: "Custom template.Variable entries stored under toolConfig.<tool>.variables." },
1926
+ { name: "tool_config", type: "object", required: false, description: "JMeter-only pass-through toolConfig.jmeter object -- escape hatch for advanced/back-compat use (e.g. .zip test-plan bundles). Ignored for K6/Locust on create (use the scalar fields for those); not required for JMeter either -- prefer script/script_image/properties/env_vars/thresholds/worker_count unless you need this override." },
1927
+ { name: "service_references", type: "array", required: false, description: "chaosService identity strings; required when CHAOS_RISK_SERVICES_ENABLED is on." },
1928
+ { name: "cleanup_policy", type: "string", required: false, description: descLoadtestCleanupPolicy },
1929
+ { name: "max_duration_sec", type: "number", required: false, description: "Per-load-test hard cap on any run." },
1930
+ { name: "resources", type: "object", required: false, description: descLoadtestResources },
1931
+ ],
1932
+ },
1933
+ },
1934
+ update: {
1935
+ method: "PUT",
1936
+ path: `${CHAOS_LOADTEST}/v1/load-tests/{loadtestId}`,
1937
+ operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
1938
+ pathParams: { loadtest_id: "loadtestId" },
1939
+ skipScopeBodyInjection: true,
1940
+ bodyBuilder: (input) => {
1941
+ const b = coerceBody(input);
1942
+ rejectLegacyLoadtestFields(b);
1943
+ const body = {};
1944
+ if (b.name != null)
1945
+ body.name = b.name;
1946
+ if (b.description != null)
1947
+ body.description = b.description;
1948
+ if (b.tags != null)
1949
+ body.tags = b.tags;
1950
+ if (b.service_references != null)
1951
+ body.serviceReferences = b.service_references;
1952
+ if (b.environment_id != null)
1953
+ body.environmentIdentifier = b.environment_id;
1954
+ if (b.infra_id != null)
1955
+ body.infraIdentifier = b.infra_id;
1956
+ if (b.target_type != null)
1957
+ body.targetType = b.target_type;
1958
+ if (b.max_duration_sec != null)
1959
+ body.maxDurationSec = b.max_duration_sec;
1960
+ if (b.cleanup_policy != null)
1961
+ body.cleanupPolicy = b.cleanup_policy;
1962
+ if (b.resources != null)
1963
+ body.resources = b.resources;
1964
+ // tool_type is never sent to the API (immutable after creation) --
1965
+ // used only internally to pick the right scalar builder / wrap key.
1966
+ const toolType = b.tool_type;
1967
+ const toolConfigEscapeHatch = b.tool_config;
1968
+ // Same scalar surface as create -- if any of these are present,
1969
+ // toolConfig.<tool> must be rebuilt wholesale (server does a full
1970
+ // overwrite; see UpdateLoadTestRequest.ToolConfig in loadTestManager).
1971
+ const SCALAR_TOOL_FIELDS = [
1972
+ "target_url", "script_source", "script", "script_image", "script_entrypoint",
1973
+ "load_args", "image_pull_secret", "users", "spawn_rate", "duration_sec",
1974
+ "ramp_up_sec", "worker_count", "host_url", "rps_limit", "iterations",
1975
+ "env_vars", "properties", "thresholds",
1976
+ ];
1977
+ const hasScalarToolField = SCALAR_TOOL_FIELDS.some((f) => b[f] != null);
1978
+ if (hasScalarToolField) {
1979
+ if (toolType == null) {
1980
+ throw new Error("tool_type is required to edit script/tunables/properties/thresholds/env_vars on update -- call harness_get first if you don't know the existing tool_type.");
1981
+ }
1982
+ if (toolType !== "Locust" && toolType !== "K6" && toolType !== "JMeter") {
1983
+ throw new Error(`tool_type '${toolType}' must be 'Locust', 'K6', or 'JMeter'.`);
1984
+ }
1985
+ const targetUrl = (b.target_url ?? b.targetUrl);
1986
+ const script = b.script;
1987
+ const scriptImage = (b.script_image ?? b.scriptImage);
1988
+ const scriptSource = b.script_source ?? (scriptImage != null ? "image" : "inline");
1989
+ let toolBlock;
1990
+ if (toolType === "K6") {
1991
+ toolBlock = buildK6ToolConfig(b, { scriptSource, script, targetUrl });
1992
+ }
1993
+ else if (toolType === "Locust") {
1994
+ toolBlock = buildLocustToolConfig(b, { scriptSource, script, targetUrl });
1995
+ }
1996
+ else {
1997
+ toolBlock = buildJMeterToolConfig(b, { scriptSource, script });
1998
+ }
1999
+ // Mirrors create: variables live under toolConfig.<tool>.variables,
2000
+ // which ReconcileToolConfigVariables prefers over the top-level
2001
+ // field -- do not also set body.variables to avoid two sources.
2002
+ if (Array.isArray(b.variables)) {
2003
+ toolBlock.variables = b.variables;
2004
+ }
2005
+ body.toolConfig = { [toolType.toLowerCase()]: toolBlock };
2006
+ }
2007
+ else if (toolConfigEscapeHatch != null) {
2008
+ if (toolType != null) {
2009
+ const toolKey = toolType.toLowerCase();
2010
+ const inner = toolConfigEscapeHatch[toolKey] ??
2011
+ toolConfigEscapeHatch;
2012
+ body.toolConfig = { [toolKey]: inner };
2013
+ }
2014
+ else {
2015
+ // Back-compat: caller already sends the fully-wired { <tool>: {...} } shape.
2016
+ body.toolConfig = toolConfigEscapeHatch;
2017
+ }
2018
+ if (b.variables != null)
2019
+ body.variables = b.variables;
2020
+ }
2021
+ else if (b.variables != null) {
2022
+ // No toolConfig rebuild in this call -- variables-only update,
2023
+ // top-level field (server falls back to it when
2024
+ // toolConfig.<tool>.variables is empty).
2025
+ body.variables = b.variables;
2026
+ }
2027
+ return body;
2028
+ },
2029
+ responseExtractor: chaosLoadTestExtract,
2030
+ description: descUpdateLoadtest,
2031
+ bodySchema: {
2032
+ description: descUpdateLoadtest,
2033
+ fields: [
2034
+ { name: "name", type: "string", required: false, description: descLoadtestName },
1677
2035
  { name: "description", type: "string", required: false, description: descLoadtestDescription },
1678
2036
  { name: "tags", type: "array", required: false, description: descLoadtestTags },
2037
+ { name: "service_references", type: "array", required: false, description: "chaosService identity strings; a non-null slice must be non-empty." },
2038
+ { name: "environment_id", type: "string", required: false, description: descLoadtestEnvId },
2039
+ { name: "infra_id", type: "string", required: false, description: descLoadtestInfraId },
1679
2040
  { name: "target_type", type: "string", required: false, description: descLoadtestTargetType },
2041
+ { name: "max_duration_sec", type: "number", required: false, description: "Per-load-test hard cap on any run." },
2042
+ { name: "cleanup_policy", type: "string", required: false, description: descLoadtestCleanupPolicy },
2043
+ { name: "resources", type: "object", required: false, description: descLoadtestResources },
1680
2044
  { name: "tool_type", type: "string", required: false, description: descLoadtestType },
2045
+ { name: "target_url", type: "string", required: false, description: descLoadtestTargetUrl },
2046
+ { name: "script_source", type: "string", required: false, description: descLoadtestScriptSource },
2047
+ { name: "script", type: "string", required: false, description: descLoadtestScript },
2048
+ { name: "script_image", type: "string", required: false, description: descLoadtestScriptImage },
2049
+ { name: "script_entrypoint", type: "string", required: false, description: descLoadtestScriptEntrypoint },
2050
+ { name: "load_args", type: "string", required: false, description: descLoadtestLoadArgs },
2051
+ { name: "image_pull_secret", type: "string", required: false, description: descLoadtestImagePullSecret },
1681
2052
  { name: "users", type: "number", required: false, description: descLoadtestUsers },
2053
+ { name: "spawn_rate", type: "number", required: false, description: "Locust spawn rate (users/sec)." },
1682
2054
  { name: "duration_sec", type: "number", required: false, description: descLoadtestDurationSec },
1683
2055
  { name: "ramp_up_sec", type: "number", required: false, description: descLoadtestRampUpSec },
1684
2056
  { name: "worker_count", type: "number", required: false, description: descLoadtestWorkerCount },
@@ -1686,6 +2058,10 @@ export const chaosToolset = {
1686
2058
  { name: "rps_limit", type: "number", required: false, description: descLoadtestRpsLimit },
1687
2059
  { name: "iterations", type: "number", required: false, description: descLoadtestIterations },
1688
2060
  { name: "env_vars", type: "array", required: false, description: descLoadtestEnvVars },
2061
+ { name: "properties", type: "array", required: false, description: descLoadtestProperties },
2062
+ { name: "thresholds", type: "array", required: false, description: descLoadtestThresholds },
2063
+ { name: "tool_config", type: "object", required: false, description: "Advanced escape hatch to hand-construct toolConfig.<tool> directly (e.g. JMeter .zip bundles) instead of the scalar fields above. Pass 'tool_type' alongside it so MCP wraps/unwraps it consistently with create; without 'tool_type' it is sent to the API exactly as given (must already be the full '{ <tool>: {...} }' wire shape). Full replacement of toolConfig.<tool> either way -- null/omit to keep existing." },
2064
+ { name: "variables", type: "array", required: false, description: "Full replacement variables list; null to keep existing. When also editing script/tunables via the scalar fields above, this is nested into toolConfig.<tool>.variables instead of sent top-level (mirrors create)." },
1689
2065
  ],
1690
2066
  },
1691
2067
  },
@@ -1704,13 +2080,32 @@ export const chaosToolset = {
1704
2080
  path: `${CHAOS_LOADTEST}/v1/load-tests/{loadtestId}/runs`,
1705
2081
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
1706
2082
  pathParams: { loadtest_id: "loadtestId" },
1707
- // TODO(runtime-inputs): accept { values: TemplateInputMinimum[] } when runtime
1708
- // inputs ('<+input>' sentinel) are added (see deferred follow-up plan). The empty
1709
- // body is correct while runtime inputs are unsupported on the create side.
1710
- bodyBuilder: () => ({}),
2083
+ // CreateLoadTestRunRequest.Identity is binding:"required" send a UUID
2084
+ // when the caller doesn't supply one so ShouldBindJSON succeeds.
2085
+ bodyBuilder: (input) => {
2086
+ const b = coerceBody(input);
2087
+ const body = {
2088
+ identity: b.run_identity ?? randomUUID(),
2089
+ };
2090
+ if (b.run_name != null)
2091
+ body.name = b.run_name;
2092
+ if (b.values != null)
2093
+ body.values = b.values;
2094
+ if (b.runtime_values != null)
2095
+ body.runtimeValues = b.runtime_values;
2096
+ return body;
2097
+ },
1711
2098
  responseExtractor: passthrough,
1712
2099
  actionDescription: descRunLoadtest,
1713
- bodySchema: { description: descBodyNoBody, fields: [] },
2100
+ bodySchema: {
2101
+ description: "Run parameters (all optional).",
2102
+ fields: [
2103
+ { name: "run_identity", type: "string", required: false, description: "Client-supplied identity for the run; a UUID is generated when omitted." },
2104
+ { name: "run_name", type: "string", required: false, description: "Optional display name for the run." },
2105
+ { name: "values", type: "array", required: false, description: "Runtime override values for load-test Variables (template.InputMinimum entries)." },
2106
+ { name: "runtime_values", type: "object", required: false, description: "Path-keyed runtime overrides for toolConfig '<+input>' leaves." },
2107
+ ],
2108
+ },
1714
2109
  },
1715
2110
  stop: {
1716
2111
  method: "POST",
@@ -1724,6 +2119,259 @@ export const chaosToolset = {
1724
2119
  },
1725
2120
  },
1726
2121
  },
2122
+ // ── Chaos Service (Service Management) ────────────────────────────
2123
+ // v3 REST endpoints under the chaos manager:
2124
+ // GET /v3/chaos-services
2125
+ // GET /v3/chaos-services/{identity}
2126
+ // POST /v3/chaos-services
2127
+ // PUT /v3/chaos-services/{identity}
2128
+ // DELETE /v3/chaos-services/{identity}
2129
+ // List response envelope is
2130
+ // { data: [...], correlationID, pagination: { totalItems, ... } }
2131
+ // (distinct from the load-test v1 envelope of { items, pagination }).
2132
+ // Create and update return the full ChaosServiceResponse. Update is a
2133
+ // full-replace on mutable fields plus desired-state reconcile of probes.
2134
+ // Delete soft-deletes the service and purges its probe mappings, returning
2135
+ // { success, correlationID }.
2136
+ {
2137
+ resourceType: "chaos_service",
2138
+ displayName: "Chaos Service",
2139
+ description: descChaosService,
2140
+ toolset: "chaos",
2141
+ scope: "project",
2142
+ scopeParams: CHAOS_SCOPE,
2143
+ identifierFields: ["identity"],
2144
+ deepLinkTemplate: "/ng/account/{accountId}/module/chaos/orgs/{orgIdentifier}/projects/{projectIdentifier}/risks/services/{identity}",
2145
+ listFilterFields: [
2146
+ { name: "environment_ids", description: descChaosServiceEnvironmentIds },
2147
+ { name: "infrastructure_ids", description: descChaosServiceInfrastructureIds },
2148
+ { name: "tags", description: descChaosServiceTags },
2149
+ { name: "include_all_scope", description: descChaosServiceIncludeAllScope, type: "boolean" },
2150
+ { name: "search", description: descChaosServiceSearch },
2151
+ { name: "probe_ids", description: descChaosServiceProbeIds },
2152
+ { name: "onboarding_id", description: descChaosServiceOnboardingIdFilter },
2153
+ ],
2154
+ relatedResources: [
2155
+ { resourceType: "discovered_agent", relationship: "prerequisite", description: "STEP 1 of create: pick the Service Discovery agent. Its 'identity' becomes agent_id, its environmentIdentifier becomes environment_id, and (for SD K8s) its identity is also the bare infrastructure_id." },
2156
+ { resourceType: "discovered_namespace", relationship: "filters", description: "Optional STEP 2a: list namespaces for the chosen agent to narrow the discovered_service list before picking a service." },
2157
+ { resourceType: "discovered_service", relationship: "prerequisite", description: "STEP 2b of create: pick the service to onboard. Its 'id' becomes external_service_id (the server derives serviceType/namespace from it)." },
2158
+ { resourceType: "chaos_probe", relationship: "associates", description: "STEP 4 of create: optional health-check probes to attach. Source probe identities and their inputs[] schema here; fill each input value before adding to the create body's probes array." },
2159
+ ],
2160
+ operations: {
2161
+ list: {
2162
+ method: "GET",
2163
+ path: `${CHAOS}/v3/chaos-services`,
2164
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
2165
+ queryParams: {
2166
+ page: "page",
2167
+ limit: "limit",
2168
+ size: "limit",
2169
+ search: "search",
2170
+ search_term: "search",
2171
+ environment_ids: "environmentIds",
2172
+ infrastructure_ids: "infrastructureIds",
2173
+ tags: "tags",
2174
+ include_all_scope: "includeAllScope",
2175
+ sort_field: "sortField",
2176
+ sort_ascending: "sortAscending",
2177
+ probe_ids: "probeIds",
2178
+ onboarding_id: "onboardingId",
2179
+ },
2180
+ responseExtractor: chaosServiceListExtract,
2181
+ description: descListChaosServices,
2182
+ },
2183
+ get: {
2184
+ method: "GET",
2185
+ path: `${CHAOS}/v3/chaos-services/{identity}`,
2186
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
2187
+ pathParams: { identity: "identity" },
2188
+ responseExtractor: passthrough,
2189
+ description: descGetChaosService,
2190
+ },
2191
+ delete: {
2192
+ method: "DELETE",
2193
+ path: `${CHAOS}/v3/chaos-services/{identity}`,
2194
+ operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
2195
+ pathParams: { identity: "identity" },
2196
+ responseExtractor: passthrough,
2197
+ description: descDeleteChaosService,
2198
+ },
2199
+ create: {
2200
+ method: "POST",
2201
+ path: `${CHAOS}/v3/chaos-services`,
2202
+ operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
2203
+ bodyBuilder: (input) => {
2204
+ const b = coerceBody(input);
2205
+ const externalServiceId = b.external_service_id ?? b.externalServiceId;
2206
+ const agentId = b.agent_id ?? b.agentId;
2207
+ const environmentId = b.environment_id ?? b.environmentId;
2208
+ const infrastructureId = b.infrastructure_id ?? b.infrastructureId;
2209
+ const infrastructureType = b.infrastructure_type ?? b.infrastructureType;
2210
+ const onboardingId = b.onboarding_id ?? b.onboardingId;
2211
+ const tags = b.tags;
2212
+ const probes = b.probes?.map((p) => ({
2213
+ probeId: p.probeId ?? p.probe_id,
2214
+ ...(p.inputs ? { inputs: p.inputs } : {}),
2215
+ }));
2216
+ return {
2217
+ identity: b.identity,
2218
+ name: b.name,
2219
+ ...(b.description ? { description: b.description } : {}),
2220
+ ...(tags
2221
+ ? {
2222
+ tags: Array.isArray(tags)
2223
+ ? tags
2224
+ : String(tags)
2225
+ .split(",")
2226
+ .map((t) => t.trim())
2227
+ .filter(Boolean),
2228
+ }
2229
+ : {}),
2230
+ externalServiceId,
2231
+ agentId,
2232
+ environmentId,
2233
+ infrastructureId,
2234
+ // Dual-write snake_case keys so registry required-field validation
2235
+ // (which checks bodySchema.fields names against this transformed
2236
+ // payload) sees the fields it expects. See getBodySchemaValidationPayload
2237
+ // in registry/index.ts.
2238
+ external_service_id: externalServiceId,
2239
+ agent_id: agentId,
2240
+ environment_id: environmentId,
2241
+ infrastructure_id: infrastructureId,
2242
+ ...(infrastructureType ? { infrastructureType } : {}),
2243
+ ...(onboardingId ? { onboardingId } : {}),
2244
+ ...(probes && probes.length > 0 ? { probes } : {}),
2245
+ };
2246
+ },
2247
+ responseExtractor: passthrough,
2248
+ description: descCreateChaosService,
2249
+ bodySchema: {
2250
+ description: descBodyChaosServiceCreate,
2251
+ fields: [
2252
+ { name: "identity", type: "string", required: true, description: descChaosServiceIdentity },
2253
+ { name: "name", type: "string", required: true, description: descChaosServiceName },
2254
+ { name: "external_service_id", type: "string", required: true, description: descChaosServiceExternalServiceId },
2255
+ { name: "agent_id", type: "string", required: true, description: descChaosServiceAgentId },
2256
+ { name: "environment_id", type: "string", required: true, description: descChaosServiceEnvironmentId },
2257
+ { name: "infrastructure_id", type: "string", required: true, description: descChaosServiceInfrastructureId },
2258
+ { name: "infrastructure_type", type: "string", required: false, description: descChaosServiceInfrastructureType },
2259
+ { name: "description", type: "string", required: false, description: descChaosServiceDescription },
2260
+ { name: "tags", type: "array", required: false, description: descChaosServiceTagsBody },
2261
+ { name: "onboarding_id", type: "string", required: false, description: descChaosServiceOnboardingId },
2262
+ { name: "probes", type: "array", required: false, description: descChaosServiceProbes },
2263
+ ],
2264
+ },
2265
+ },
2266
+ update: {
2267
+ method: "PUT",
2268
+ path: `${CHAOS}/v3/chaos-services/{identity}`,
2269
+ operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
2270
+ pathParams: { identity: "identity" },
2271
+ bodyBuilder: (input) => {
2272
+ const b = coerceBody(input);
2273
+ const externalServiceId = b.external_service_id ?? b.externalServiceId;
2274
+ const agentId = b.agent_id ?? b.agentId;
2275
+ const environmentId = b.environment_id ?? b.environmentId;
2276
+ const infrastructureId = b.infrastructure_id ?? b.infrastructureId;
2277
+ const tags = b.tags;
2278
+ const probes = b.probes?.map((p) => ({
2279
+ probeId: p.probeId ?? p.probe_id,
2280
+ ...(p.inputs ? { inputs: p.inputs } : {}),
2281
+ }));
2282
+ return {
2283
+ name: b.name,
2284
+ ...(b.description !== undefined ? { description: b.description } : {}),
2285
+ ...(tags
2286
+ ? {
2287
+ tags: Array.isArray(tags)
2288
+ ? tags
2289
+ : String(tags)
2290
+ .split(",")
2291
+ .map((t) => t.trim())
2292
+ .filter(Boolean),
2293
+ }
2294
+ : {}),
2295
+ externalServiceId,
2296
+ agentId,
2297
+ environmentId,
2298
+ infrastructureId,
2299
+ // Dual-write snake_case keys — see comment in create bodyBuilder above.
2300
+ external_service_id: externalServiceId,
2301
+ agent_id: agentId,
2302
+ environment_id: environmentId,
2303
+ infrastructure_id: infrastructureId,
2304
+ // Full desired-state replace (hce-saas reconcileProbeMappings):
2305
+ // pass an explicit [] straight through (detach all) rather than
2306
+ // dropping it, but leave the key absent when the caller omitted it
2307
+ // so the required-field validation below rejects the request
2308
+ // instead of silently clearing every probe.
2309
+ ...(probes !== undefined ? { probes } : {}),
2310
+ };
2311
+ },
2312
+ responseExtractor: passthrough,
2313
+ description: descUpdateChaosService,
2314
+ bodySchema: {
2315
+ description: descBodyChaosServiceUpdate,
2316
+ fields: [
2317
+ { name: "name", type: "string", required: true, description: descChaosServiceName },
2318
+ { name: "external_service_id", type: "string", required: true, description: descChaosServiceExternalServiceId },
2319
+ { name: "agent_id", type: "string", required: true, description: descChaosServiceAgentId },
2320
+ { name: "environment_id", type: "string", required: true, description: descChaosServiceEnvironmentId },
2321
+ { name: "infrastructure_id", type: "string", required: true, description: descChaosServiceInfrastructureId },
2322
+ { name: "description", type: "string", required: false, description: descChaosServiceDescription },
2323
+ { name: "tags", type: "array", required: false, description: descChaosServiceTagsBody },
2324
+ { name: "probes", type: "array", required: true, description: descChaosServiceProbesUpdate },
2325
+ ],
2326
+ },
2327
+ },
2328
+ },
2329
+ executeActions: {
2330
+ list_experiment_runs: {
2331
+ method: "GET",
2332
+ path: `${CHAOS}/v3/chaos-services/{identity}/experiment-runs`,
2333
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
2334
+ pathParams: { identity: "identity" },
2335
+ queryParams: {
2336
+ page: "page",
2337
+ limit: "limit",
2338
+ size: "limit",
2339
+ search: "search",
2340
+ sort_field: "sortField",
2341
+ sort_ascending: "sortAscending",
2342
+ include_all_scope: "includeAllScope",
2343
+ infra_ids: "infraIds",
2344
+ statuses: "statuses",
2345
+ step_types: "stepTypes",
2346
+ },
2347
+ responseExtractor: passthrough,
2348
+ actionDescription: descListChaosServiceExperimentRuns,
2349
+ bodySchema: { description: descBodyNoBody, fields: [] },
2350
+ },
2351
+ list_load_tests: {
2352
+ method: "GET",
2353
+ path: `${CHAOS}/v3/chaos-services/{identity}/load-tests`,
2354
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
2355
+ pathParams: { identity: "identity" },
2356
+ queryParams: {
2357
+ page: "page",
2358
+ limit: "limit",
2359
+ size: "limit",
2360
+ search: "search",
2361
+ sort_field: "sortField",
2362
+ sort_ascending: "sortAscending",
2363
+ include_all_scope: "includeAllScope",
2364
+ tool_type: "toolType",
2365
+ environment_ids: "environmentIds",
2366
+ infra_ids: "infraIds",
2367
+ tags: "tags",
2368
+ },
2369
+ responseExtractor: passthrough,
2370
+ actionDescription: descListChaosServiceLoadTests,
2371
+ bodySchema: { description: descBodyNoBody, fields: [] },
2372
+ },
2373
+ },
2374
+ },
1727
2375
  // ── Chaos Kubernetes Infrastructure ──────────────────────────────
1728
2376
  {
1729
2377
  resourceType: "chaos_k8s_infrastructure",
@@ -1733,7 +2381,13 @@ export const chaosToolset = {
1733
2381
  scope: "project",
1734
2382
  scopeParams: CHAOS_SCOPE,
1735
2383
  identifierFields: ["infra_id"],
2384
+ deepLinkTemplate: "/ng/account/{accountId}/module/chaos/orgs/{orgIdentifier}/projects/{projectIdentifier}/settings/chaos/infrastructures/{infraId}",
1736
2385
  diagnosticHint: "An infrastructure can only create chaos experiments when status is ACTIVE and isChaosEnabled is true. Filter out any infrastructure that does not meet both conditions.",
2386
+ relatedResources: [
2387
+ { resourceType: "chaos_environment", relationship: "scoped_by", description: "STEP 1 of create: pick the Harness environment. Its identifier becomes environment_id." },
2388
+ { resourceType: "infrastructure", relationship: "prerequisite", description: "STEP 2 of create: pick the backing CD Kubernetes infrastructure definition in that environment. Its identifier becomes infra_id (defaults to chaos identity when omitted)." },
2389
+ { resourceType: "chaos_enabled_infrastructure", relationship: "child", description: "After install completes, the registered infra appears here once status=ACTIVE and isChaosEnabled=true." },
2390
+ ],
1737
2391
  listFilterFields: [
1738
2392
  { name: "environment_id", description: descEnvironmentId },
1739
2393
  { name: "status", description: descK8sInfraStatus, enum: ["ACTIVE", "INACTIVE", "PENDING", "All"] },
@@ -1776,6 +2430,72 @@ export const chaosToolset = {
1776
2430
  responseExtractor: passthrough,
1777
2431
  description: descGetK8sInfra,
1778
2432
  },
2433
+ create: {
2434
+ method: "POST",
2435
+ path: `${CHAOS}/rest/v2/infrastructure`,
2436
+ operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
2437
+ bodyBuilder: (input) => {
2438
+ const b = coerceBody(input);
2439
+ const identity = b.identity;
2440
+ const name = b.name;
2441
+ const environmentId = (b.environment_id ?? b.environmentId);
2442
+ if (!identity)
2443
+ throw new Error("identity is required.");
2444
+ if (!name)
2445
+ throw new Error("name is required.");
2446
+ if (!environmentId)
2447
+ throw new Error("environment_id is required.");
2448
+ const infraId = (b.infra_id ?? b.infraId ?? identity);
2449
+ const infraNamespace = (b.infra_namespace ?? b.infraNamespace ?? "hce");
2450
+ const serviceAccount = (b.service_account ?? b.serviceAccount ?? "litmus");
2451
+ const infraScope = (b.infra_scope ?? b.infraScope ?? "CLUSTER").toUpperCase();
2452
+ const infraType = (b.infra_type ?? b.infraType ?? "KUBERNETES").toUpperCase();
2453
+ const k8sConnectorId = (b.k8s_connector_id ?? b.k8sConnectorID ?? b.k8sConnectorId);
2454
+ const discoveryAgentId = (b.discovery_agent_id ?? b.discoveryAgentID ?? b.discoveryAgentId);
2455
+ const aiEnabled = Boolean(b.ai_enabled ?? b.aiEnabled ?? false);
2456
+ const rawTags = b.tags;
2457
+ const tags = Array.isArray(rawTags)
2458
+ ? rawTags
2459
+ : typeof rawTags === "string"
2460
+ ? rawTags.split(",").map((t) => t.trim()).filter(Boolean)
2461
+ : [];
2462
+ return {
2463
+ identity,
2464
+ name,
2465
+ environmentID: environmentId,
2466
+ infraID: infraId,
2467
+ infraNamespace,
2468
+ serviceAccount,
2469
+ infraScope,
2470
+ infraType,
2471
+ ...(b.description ? { description: b.description } : {}),
2472
+ ...(tags.length > 0 ? { tags } : {}),
2473
+ ...(k8sConnectorId ? { k8sConnectorID: k8sConnectorId } : {}),
2474
+ ...(discoveryAgentId ? { discoveryAgentID: discoveryAgentId } : {}),
2475
+ aiEnabled,
2476
+ insecureSkipVerify: Boolean(b.insecure_skip_verify ?? b.insecureSkipVerify ?? false),
2477
+ };
2478
+ },
2479
+ responseExtractor: passthrough,
2480
+ description: descCreateK8sInfra,
2481
+ bodySchema: {
2482
+ description: descBodyK8sInfraCreate,
2483
+ fields: [
2484
+ { name: "identity", type: "string", required: true, description: descK8sInfraIdentityCreate },
2485
+ { name: "name", type: "string", required: true, description: descK8sInfraNameCreate },
2486
+ { name: "environment_id", type: "string", required: true, description: descK8sInfraEnvironmentIdCreate },
2487
+ { name: "infra_id", type: "string", required: false, description: descK8sInfraInfraIdCreate },
2488
+ { name: "k8s_connector_id", type: "string", required: false, description: descK8sInfraConnectorIdCreate },
2489
+ { name: "infra_namespace", type: "string", required: false, description: descK8sInfraNamespaceCreate },
2490
+ { name: "service_account", type: "string", required: false, description: descK8sInfraServiceAccountCreate },
2491
+ { name: "infra_scope", type: "string", required: false, description: descK8sInfraScopeCreate },
2492
+ { name: "infra_type", type: "string", required: false, description: descK8sInfraTypeCreate },
2493
+ { name: "description", type: "string", required: false, description: "Optional description for the chaos server." },
2494
+ { name: "tags", type: "array", required: false, description: "Optional tags (array of strings or comma-separated string)." },
2495
+ { name: "ai_enabled", type: "boolean", required: false, description: descK8sInfraAiEnabledCreate },
2496
+ ],
2497
+ },
2498
+ },
1779
2499
  },
1780
2500
  executeActions: {
1781
2501
  check_health: {
@@ -3032,6 +3752,60 @@ export const chaosToolset = {
3032
3752
  },
3033
3753
  },
3034
3754
  },
3755
+ // ── Service Discovery: Agents ──────────────────────────────────────
3756
+ // GET /gateway/servicediscovery/api/v1/agents (service-discovery repo).
3757
+ // Envelope matches the other SD list endpoints:
3758
+ // { items, page: { totalItems, ... }, correlationID }
3759
+ // so we reuse sdPageExtract. The Go handler treats environmentIdentifier
3760
+ // as an optional narrowing filter even though swagger marks it required —
3761
+ // omit to enumerate agents across every environment in the current scope.
3762
+ {
3763
+ resourceType: "discovered_agent",
3764
+ displayName: "Discovered Agent",
3765
+ description: descDiscoveredAgent,
3766
+ toolset: "chaos",
3767
+ scope: "project",
3768
+ scopeParams: CHAOS_SCOPE,
3769
+ identifierFields: ["identity"],
3770
+ compactItem: compactDiscoveredAgent,
3771
+ searchAliases: [
3772
+ "service discovery agent", "sd agent", "discovery agent",
3773
+ "chaos discovery agent",
3774
+ ],
3775
+ listFilterFields: [
3776
+ { name: "environment_id", description: descSDAgentListEnvironmentId },
3777
+ { name: "search", description: descDiscoveredAgentSearch },
3778
+ { name: "all", type: "boolean", description: descSDFetchAll },
3779
+ ],
3780
+ relatedResources: [
3781
+ {
3782
+ resourceType: "discovered_namespace",
3783
+ relationship: "produces",
3784
+ description: "The 'identity' field returned here is the value discovered_namespace / discovered_service / discovered_network_map take as agent_identity.",
3785
+ },
3786
+ {
3787
+ resourceType: "chaos_service",
3788
+ relationship: "referenced_by",
3789
+ description: "Use an agent's 'identity' as the agent_id field when creating or updating a chaos_service.",
3790
+ },
3791
+ ],
3792
+ operations: {
3793
+ list: {
3794
+ method: "GET",
3795
+ path: `${SD}/agents`,
3796
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
3797
+ queryParams: {
3798
+ environment_id: "environmentIdentifier",
3799
+ search: "search",
3800
+ page: "page",
3801
+ size: "limit", // SD uses `limit`, not `size`
3802
+ all: "all",
3803
+ },
3804
+ responseExtractor: sdPageExtract,
3805
+ description: descListDiscoveredAgents,
3806
+ },
3807
+ },
3808
+ },
3035
3809
  // ── Service Discovery: Namespaces ──────────────────────────────────
3036
3810
  {
3037
3811
  resourceType: "discovered_namespace",