You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by be...@apache.org on 2022/10/04 15:17:44 UTC

[nifi] branch main updated: NIFI-10531 Add supported operations to c2 heartbeat:

This is an automated email from the ASF dual-hosted git repository.

bejancsaba pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 504baae227 NIFI-10531 Add supported operations to c2 heartbeat:
504baae227 is described below

commit 504baae227860e2852c2740fed7c366803a89117
Author: Ferenc Erdei <er...@gmail.com>
AuthorDate: Wed Sep 21 15:09:09 2022 +0200

    NIFI-10531 Add supported operations to c2 heartbeat:
    
    This closes #6438
    
    Signed-off-by: Csaba Bejan <be...@gmail.com>
---
 .../nifi/c2/client/service/C2ClientService.java    |  8 ++-
 .../nifi/c2/client/service/C2HeartbeatFactory.java | 47 +++++--------
 .../c2/client/service/ManifestHashProvider.java    | 81 +++++++++++++++++++++
 .../service/operation/C2OperationHandler.java      |  8 +++
 .../service/operation/C2OperationService.java      | 14 ++++
 .../service/operation/DebugOperationHandler.java   | 16 ++++-
 .../DescribeManifestOperationHandler.java          | 15 ++--
 ...er.java => EmptyOperandPropertiesProvider.java} | 38 +++-------
 ...Handler.java => OperandPropertiesProvider.java} | 36 +++-------
 .../operation/SupportedOperationsProvider.java     | 54 ++++++++++++++
 .../UpdateConfigurationOperationHandler.java       | 10 ++-
 .../c2/client/service/C2HeartbeatFactoryTest.java  | 64 +++++++++--------
 .../client/service/ManifestHashProviderTest.java   | 65 +++++++++++++++++
 .../service/operation/C2OperationServiceTest.java  | 46 +++++++++---
 .../operation/DebugOperationHandlerTest.java       | 16 +++--
 .../DescribeManifestOperationHandlerTest.java      |  6 +-
 .../EmptyOperandPropertiesProviderTest.java}       | 39 +++-------
 .../operation/SupportedOperationsProviderTest.java | 71 +++++++++++++++++++
 .../UpdateConfigurationOperationHandlerTest.java   | 16 +++--
 .../apache/nifi/c2/protocol/api/AgentManifest.java | 82 ++++++++++++++++++++++
 .../nifi/c2/protocol/api/SupportedOperation.java   | 76 ++++++++++++++++++++
 .../main/markdown/minifi-java-agent-quick-start.md | 15 ++++
 .../org/apache/nifi/c2/C2NifiClientService.java    | 33 +++++----
 23 files changed, 666 insertions(+), 190 deletions(-)

diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2ClientService.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2ClientService.java
index c9424c67c2..67d406086e 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2ClientService.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2ClientService.java
@@ -41,8 +41,12 @@ public class C2ClientService {
     }
 
     public void sendHeartbeat(RuntimeInfoWrapper runtimeInfoWrapper) {
-        C2Heartbeat c2Heartbeat = c2HeartbeatFactory.create(runtimeInfoWrapper);
-        client.publishHeartbeat(c2Heartbeat).ifPresent(this::processResponse);
+        try {
+            C2Heartbeat c2Heartbeat = c2HeartbeatFactory.create(runtimeInfoWrapper);
+            client.publishHeartbeat(c2Heartbeat).ifPresent(this::processResponse);
+        } catch (Exception e) {
+            logger.error("Failed to send/process heartbeat:", e);
+        }
     }
 
     private void processResponse(C2HeartbeatResponse response) {
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2HeartbeatFactory.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2HeartbeatFactory.java
index 063017ea5a..92d85720d0 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2HeartbeatFactory.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/C2HeartbeatFactory.java
@@ -23,12 +23,9 @@ import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
 import java.net.InetAddress;
 import java.net.NetworkInterface;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -37,6 +34,7 @@ import org.apache.nifi.c2.client.C2ClientConfig;
 import org.apache.nifi.c2.client.PersistentUuidGenerator;
 import org.apache.nifi.c2.client.service.model.RuntimeInfoWrapper;
 import org.apache.nifi.c2.protocol.api.AgentInfo;
+import org.apache.nifi.c2.protocol.api.AgentManifest;
 import org.apache.nifi.c2.protocol.api.AgentRepositories;
 import org.apache.nifi.c2.protocol.api.AgentStatus;
 import org.apache.nifi.c2.protocol.api.C2Heartbeat;
@@ -44,8 +42,8 @@ import org.apache.nifi.c2.protocol.api.DeviceInfo;
 import org.apache.nifi.c2.protocol.api.FlowInfo;
 import org.apache.nifi.c2.protocol.api.FlowQueueStatus;
 import org.apache.nifi.c2.protocol.api.NetworkInfo;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
 import org.apache.nifi.c2.protocol.api.SystemInfo;
-import org.apache.nifi.c2.protocol.component.api.Bundle;
 import org.apache.nifi.c2.protocol.component.api.RuntimeManifest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -59,14 +57,16 @@ public class C2HeartbeatFactory {
 
     private final C2ClientConfig clientConfig;
     private final FlowIdHolder flowIdHolder;
+    private final ManifestHashProvider manifestHashProvider;
 
     private String agentId;
     private String deviceId;
     private File confDirectory;
 
-    public C2HeartbeatFactory(C2ClientConfig clientConfig, FlowIdHolder flowIdHolder) {
+    public C2HeartbeatFactory(C2ClientConfig clientConfig, FlowIdHolder flowIdHolder, ManifestHashProvider manifestHashProvider) {
         this.clientConfig = clientConfig;
         this.flowIdHolder = flowIdHolder;
+        this.manifestHashProvider = manifestHashProvider;
     }
 
     public C2Heartbeat create(RuntimeInfoWrapper runtimeInfoWrapper) {
@@ -97,7 +97,7 @@ public class C2HeartbeatFactory {
         agentStatus.setRepositories(repos);
 
         agentInfo.setStatus(agentStatus);
-        agentInfo.setAgentManifestHash(calculateManifestHash(manifest.getBundles()));
+        agentInfo.setAgentManifestHash(manifestHashProvider.calculateManifestHash(manifest.getBundles(), getSupportedOperations(manifest)));
 
         if (clientConfig.isFullHeartbeat()) {
             agentInfo.setAgentManifest(manifest);
@@ -106,6 +106,17 @@ public class C2HeartbeatFactory {
         return agentInfo;
     }
 
+    private Set<SupportedOperation> getSupportedOperations(RuntimeManifest manifest) {
+        Set<SupportedOperation> supportedOperations;
+        // supported operations has value only in case of minifi, therefore we return empty collection if
+        if (manifest instanceof AgentManifest) {
+            supportedOperations = ((AgentManifest) manifest).getSupportedOperations();
+        } else {
+            supportedOperations = Collections.emptySet();
+        }
+        return supportedOperations;
+    }
+
     private String getAgentId() {
         if (agentId == null) {
             String rawAgentId = clientConfig.getAgentIdentifier();
@@ -235,26 +246,4 @@ public class C2HeartbeatFactory {
         return confDirectory;
     }
 
-    private String calculateManifestHash(List<Bundle> loadedBundles) {
-        byte[] bytes;
-        try {
-            bytes = MessageDigest.getInstance("SHA-512").digest(loadedBundles.stream()
-                .map(bundle -> bundle.getGroup() + bundle.getArtifact() + bundle.getVersion())
-                .sorted()
-                .collect(Collectors.joining(","))
-                .getBytes(StandardCharsets.UTF_8));
-        } catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException("Unable to set up manifest hash calculation due to not having support for the chosen digest algorithm", e);
-        }
-
-        return bytesToHex(bytes);
-    }
-
-    private String bytesToHex(byte[] in) {
-        final StringBuilder builder = new StringBuilder();
-        for (byte b : in) {
-            builder.append(String.format("%02x", b));
-        }
-        return builder.toString();
-    }
 }
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/ManifestHashProvider.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/ManifestHashProvider.java
new file mode 100644
index 0000000000..4def4970fb
--- /dev/null
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/ManifestHashProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.client.service;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
+import org.apache.nifi.c2.protocol.component.api.Bundle;
+
+public class ManifestHashProvider {
+    private String currentBundles = null;
+    private Set<SupportedOperation> currentSupportedOperations = Collections.emptySet();
+    private int currentHashCode;
+    private String currentManifestHash;
+
+    public String calculateManifestHash(List<Bundle> loadedBundles, Set<SupportedOperation> supportedOperations) {
+        String bundleString = loadedBundles.stream()
+            .map(bundle -> bundle.getGroup() + bundle.getArtifact() + bundle.getVersion())
+            .sorted()
+            .collect(Collectors.joining(","));
+        int hashCode = Objects.hash(bundleString, supportedOperations);
+        if (hashCode != currentHashCode
+            || !(Objects.equals(bundleString, currentBundles) && Objects.equals(supportedOperations, currentSupportedOperations))) {
+            byte[] bytes;
+            try {
+                bytes = MessageDigest.getInstance("SHA-512").digest(getBytes(supportedOperations, bundleString));
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException("Unable to set up manifest hash calculation due to not having support for the chosen digest algorithm", e);
+            }
+            currentHashCode = hashCode;
+            currentManifestHash = bytesToHex(bytes);
+            currentBundles = bundleString;
+            currentSupportedOperations = supportedOperations;
+        }
+        return currentManifestHash;
+    }
+
+    private byte[] getBytes(Set<SupportedOperation> supportedOperations, String bundleString) {
+        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream oos = new ObjectOutputStream(bos)) {
+            oos.write(bundleString.getBytes(StandardCharsets.UTF_8));
+            oos.writeObject(supportedOperations);
+            oos.flush();
+            return bos.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to transform supportedOperations and bundles to byte array", e);
+        }
+    }
+
+    private String bytesToHex(byte[] in) {
+        final StringBuilder builder = new StringBuilder();
+        for (byte b : in) {
+            builder.append(String.format("%02x", b));
+        }
+        return builder.toString();
+    }
+}
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
index e9f2db29fb..586f0e624a 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.c2.client.service.operation;
 
+import java.util.Map;
 import org.apache.nifi.c2.protocol.api.C2Operation;
 import org.apache.nifi.c2.protocol.api.C2OperationAck;
 import org.apache.nifi.c2.protocol.api.OperandType;
@@ -40,6 +41,13 @@ public interface C2OperationHandler {
      */
     OperandType getOperandType();
 
+    /**
+     * Returns the properties context for the given operand
+     *
+     * @return the property map
+     */
+    Map<String, Object> getProperties();
+
     /**
      * Handler logic for the specific C2Operation
      *
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationService.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationService.java
index 5ce9c51ef4..31e483929d 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationService.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.c2.client.service.operation;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -47,6 +48,19 @@ public class C2OperationService {
             });
     }
 
+    public Map<OperationType, Map<OperandType, C2OperationHandler>> getHandlers() {
+        Map<OperationType, Map<OperandType, C2OperationHandler>> handlers = new HashMap<>();
+        handlerMap.entrySet()
+            .forEach(operationEntry -> {
+                Map<OperandType, C2OperationHandler> operands = new HashMap<>();
+                operationEntry.getValue()
+                    .entrySet()
+                    .forEach(o -> operands.put(o.getKey(), o.getValue()));
+                handlers.put(operationEntry.getKey(), Collections.unmodifiableMap(operands));
+            });
+        return Collections.unmodifiableMap(handlers);
+    }
+
     private Optional<C2OperationHandler> getHandlerForOperation(C2Operation operation) {
         return Optional.ofNullable(handlerMap.get(operation.getOperation()))
             .map(operandMap -> operandMap.get(operation.getOperand()));
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandler.java
index 4daa369e69..33ec839c31 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandler.java
@@ -39,6 +39,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
@@ -69,14 +70,18 @@ public class DebugOperationHandler implements C2OperationHandler {
     private final C2Client c2Client;
     private final List<Path> bundleFilePaths;
     private final Predicate<String> contentFilter;
+    private final OperandPropertiesProvider operandPropertiesProvider;
 
-    private DebugOperationHandler(C2Client c2Client, List<Path> bundleFilePaths, Predicate<String> contentFilter) {
+    private DebugOperationHandler(C2Client c2Client, List<Path> bundleFilePaths, Predicate<String> contentFilter,
+                                  OperandPropertiesProvider operandPropertiesProvider) {
         this.c2Client = c2Client;
         this.bundleFilePaths = bundleFilePaths;
         this.contentFilter = contentFilter;
+        this.operandPropertiesProvider = operandPropertiesProvider;
     }
 
-    public static DebugOperationHandler create(C2Client c2Client, List<Path> bundleFilePaths, Predicate<String> contentFilter) {
+    public static DebugOperationHandler create(C2Client c2Client, List<Path> bundleFilePaths, Predicate<String> contentFilter,
+                                               OperandPropertiesProvider operandPropertiesProvider) {
         if (c2Client == null) {
             throw new IllegalArgumentException("C2Client should not be null");
         }
@@ -87,7 +92,7 @@ public class DebugOperationHandler implements C2OperationHandler {
             throw new IllegalArgumentException("Content filter should not be null");
         }
 
-        return new DebugOperationHandler(c2Client, bundleFilePaths, contentFilter);
+        return new DebugOperationHandler(c2Client, bundleFilePaths, contentFilter, operandPropertiesProvider);
     }
 
     @Override
@@ -100,6 +105,11 @@ public class DebugOperationHandler implements C2OperationHandler {
         return DEBUG;
     }
 
+    @Override
+    public Map<String, Object> getProperties() {
+        return operandPropertiesProvider.getProperties();
+    }
+
     @Override
     public C2OperationAck handle(C2Operation operation) {
         String debugCallbackUrl = operation.getArgs().get(TARGET_ARG);
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandler.java
index 2f55b2510d..1045b4acf1 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandler.java
@@ -20,6 +20,7 @@ import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.apache.nifi.c2.protocol.api.OperandType.MANIFEST;
 import static org.apache.nifi.c2.protocol.api.OperationType.DESCRIBE;
 
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Supplier;
 import org.apache.nifi.c2.client.service.C2HeartbeatFactory;
@@ -31,19 +32,18 @@ import org.apache.nifi.c2.protocol.api.C2OperationAck;
 import org.apache.nifi.c2.protocol.api.C2OperationState;
 import org.apache.nifi.c2.protocol.api.OperandType;
 import org.apache.nifi.c2.protocol.api.OperationType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class DescribeManifestOperationHandler implements C2OperationHandler {
 
-    private static final Logger logger = LoggerFactory.getLogger(DescribeManifestOperationHandler.class);
-
     private final C2HeartbeatFactory heartbeatFactory;
     private final Supplier<RuntimeInfoWrapper> runtimeInfoSupplier;
+    private final OperandPropertiesProvider operandPropertiesProvider;
 
-    public DescribeManifestOperationHandler(C2HeartbeatFactory heartbeatFactory, Supplier<RuntimeInfoWrapper> runtimeInfoSupplier) {
+    public DescribeManifestOperationHandler(C2HeartbeatFactory heartbeatFactory, Supplier<RuntimeInfoWrapper> runtimeInfoSupplier,
+        OperandPropertiesProvider operandPropertiesProvider) {
         this.heartbeatFactory = heartbeatFactory;
         this.runtimeInfoSupplier = runtimeInfoSupplier;
+        this.operandPropertiesProvider = operandPropertiesProvider;
     }
 
     @Override
@@ -78,4 +78,9 @@ public class DescribeManifestOperationHandler implements C2OperationHandler {
 
         return operationAck;
     }
+
+    @Override
+    public Map<String, Object> getProperties() {
+        return operandPropertiesProvider.getProperties();
+    }
 }
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProvider.java
similarity index 50%
copy from c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
copy to c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProvider.java
index e9f2db29fb..b9bcef1686 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProvider.java
@@ -14,37 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.c2.client.service.operation;
 
-import org.apache.nifi.c2.protocol.api.C2Operation;
-import org.apache.nifi.c2.protocol.api.C2OperationAck;
-import org.apache.nifi.c2.protocol.api.OperandType;
-import org.apache.nifi.c2.protocol.api.OperationType;
+package org.apache.nifi.c2.client.service.operation;
 
-/**
- * Handler interface for the different operation types
- */
-public interface C2OperationHandler {
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
-    /**
-     * Returns the supported OperationType by the handler
-     *
-     * @return the type of the operation
-     */
-    OperationType getOperationType();
+public class EmptyOperandPropertiesProvider implements OperandPropertiesProvider {
 
-    /**
-     * Returns the supported OperandType by the handler
-     *
-     * @return the type of the operand
-     */
-    OperandType getOperandType();
+    private static final Map<String, Object> EMPTY_MAP = Collections.unmodifiableMap(new HashMap<>());
 
-    /**
-     * Handler logic for the specific C2Operation
-     *
-     * @param operation the C2Operation to be handled
-     * @return the result of the operation handling
-     */
-    C2OperationAck handle(C2Operation operation);
+    @Override
+    public Map<String, Object> getProperties() {
+        return EMPTY_MAP;
+    }
 }
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/OperandPropertiesProvider.java
similarity index 51%
copy from c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
copy to c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/OperandPropertiesProvider.java
index e9f2db29fb..5bb853c681 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/OperandPropertiesProvider.java
@@ -14,37 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.nifi.c2.client.service.operation;
 
-import org.apache.nifi.c2.protocol.api.C2Operation;
-import org.apache.nifi.c2.protocol.api.C2OperationAck;
-import org.apache.nifi.c2.protocol.api.OperandType;
-import org.apache.nifi.c2.protocol.api.OperationType;
+import java.util.Map;
 
 /**
- * Handler interface for the different operation types
- */
-public interface C2OperationHandler {
-
-    /**
-     * Returns the supported OperationType by the handler
-     *
-     * @return the type of the operation
-     */
-    OperationType getOperationType();
-
-    /**
-     * Returns the supported OperandType by the handler
-     *
-     * @return the type of the operand
-     */
-    OperandType getOperandType();
+ * Common interface to provide properties for different operands.
+ * */
+public interface OperandPropertiesProvider {
 
     /**
-     * Handler logic for the specific C2Operation
+     * Get the properties for the given operand.
+     * The Value of the Map must implement the Serializable interface.
      *
-     * @param operation the C2Operation to be handled
-     * @return the result of the operation handling
-     */
-    C2OperationAck handle(C2Operation operation);
+     * @return the properties of the given operand
+     * */
+    Map<String, Object> getProperties();
 }
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProvider.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProvider.java
new file mode 100644
index 0000000000..d60a9fee1a
--- /dev/null
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.client.service.operation;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.nifi.c2.protocol.api.OperandType;
+import org.apache.nifi.c2.protocol.api.OperationType;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
+
+public class SupportedOperationsProvider {
+    private final Map<OperationType, Map<OperandType, C2OperationHandler>> operationHandlers;
+
+    public SupportedOperationsProvider(Map<OperationType, Map<OperandType, C2OperationHandler>> handlers) {
+        operationHandlers = handlers;
+    }
+
+    public Set<SupportedOperation> getSupportedOperations() {
+        return operationHandlers.entrySet()
+            .stream()
+            .map(operationEntry -> getSupportedOperation(operationEntry.getKey(), operationEntry.getValue()))
+            .collect(Collectors.toSet());
+    }
+
+    private SupportedOperation getSupportedOperation(OperationType operationType, Map<OperandType, C2OperationHandler> operands) {
+        SupportedOperation supportedOperation = new SupportedOperation();
+        supportedOperation.setType(operationType);
+
+        Map<OperandType, Map<String, Object>> properties = operands.values()
+            .stream()
+            .collect(Collectors.toMap(C2OperationHandler::getOperandType, C2OperationHandler::getProperties));
+
+        supportedOperation.setProperties(properties);
+
+        return supportedOperation;
+    }
+
+}
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandler.java
index dc1d15f610..f04af88714 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandler.java
@@ -45,11 +45,14 @@ public class UpdateConfigurationOperationHandler implements C2OperationHandler {
     private final C2Client client;
     private final Function<byte[], Boolean> updateFlow;
     private final FlowIdHolder flowIdHolder;
+    private final OperandPropertiesProvider operandPropertiesProvider;
 
-    public UpdateConfigurationOperationHandler(C2Client client, FlowIdHolder flowIdHolder, Function<byte[], Boolean> updateFlow) {
+    public UpdateConfigurationOperationHandler(C2Client client, FlowIdHolder flowIdHolder, Function<byte[], Boolean> updateFlow,
+        OperandPropertiesProvider operandPropertiesProvider) {
         this.client = client;
         this.updateFlow = updateFlow;
         this.flowIdHolder = flowIdHolder;
+        this.operandPropertiesProvider = operandPropertiesProvider;
     }
 
     @Override
@@ -128,4 +131,9 @@ public class UpdateConfigurationOperationHandler implements C2OperationHandler {
         }
         return null;
     }
+
+    @Override
+    public Map<String, Object> getProperties() {
+        return operandPropertiesProvider.getProperties();
+    }
 }
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/C2HeartbeatFactoryTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/C2HeartbeatFactoryTest.java
index c1c8efb86d..ccc8a01e47 100644
--- a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/C2HeartbeatFactoryTest.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/C2HeartbeatFactoryTest.java
@@ -17,7 +17,6 @@
 package org.apache.nifi.c2.client.service;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -26,28 +25,34 @@ import static org.mockito.Mockito.when;
 
 import java.io.File;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import org.apache.nifi.c2.client.C2ClientConfig;
 import org.apache.nifi.c2.client.service.model.RuntimeInfoWrapper;
+import org.apache.nifi.c2.protocol.api.AgentManifest;
 import org.apache.nifi.c2.protocol.api.AgentRepositories;
 import org.apache.nifi.c2.protocol.api.C2Heartbeat;
 import org.apache.nifi.c2.protocol.api.FlowQueueStatus;
+import org.apache.nifi.c2.protocol.api.OperationType;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
 import org.apache.nifi.c2.protocol.component.api.Bundle;
 import org.apache.nifi.c2.protocol.component.api.RuntimeManifest;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
 
-@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
 public class C2HeartbeatFactoryTest {
 
     private static final String AGENT_CLASS = "agentClass";
     private static final String FLOW_ID = "flowId";
+    private static final String MANIFEST_HASH = "hash";
 
     @Mock
     private C2ClientConfig clientConfig;
@@ -58,6 +63,9 @@ public class C2HeartbeatFactoryTest {
     @Mock
     private RuntimeInfoWrapper runtimeInfoWrapper;
 
+    @Mock
+    private ManifestHashProvider manifestHashProvider;
+
     @InjectMocks
     private C2HeartbeatFactory c2HeartbeatFactory;
 
@@ -129,33 +137,27 @@ public class C2HeartbeatFactoryTest {
     }
 
     @Test
-    void testManifestHashChangesWhenManifestBundleChanges() {
-        Bundle bundle1 = new Bundle("group1", "artifact1", "version1");
-        Bundle bundle2 = new Bundle("group2", "artifact2", "version2");
-        RuntimeManifest manifest1 = createManifest(bundle1);
-        RuntimeManifest manifest2 = createManifest(bundle2);
-        RuntimeManifest manifest3 = createManifest(bundle1, bundle2);
-
-        when(runtimeInfoWrapper.getManifest()).thenReturn(manifest1);
-        C2Heartbeat heartbeat1 = c2HeartbeatFactory.create(runtimeInfoWrapper);
-        String hash1 = heartbeat1.getAgentInfo().getAgentManifestHash();
-        assertNotNull(hash1);
-
-        // same manifest should result in the same hash
-        assertEquals(hash1,  c2HeartbeatFactory.create(runtimeInfoWrapper).getAgentInfo().getAgentManifestHash());
-
-        // different manifest should result in hash change
-        when(runtimeInfoWrapper.getManifest()).thenReturn(manifest2);
-        C2Heartbeat heartbeat2 = c2HeartbeatFactory.create(runtimeInfoWrapper);
-        String hash2 = heartbeat2.getAgentInfo().getAgentManifestHash();
-        assertNotEquals(hash2, hash1);
-
-        // different manifest with multiple bundles should result in hash change compared to all previous
-        when(runtimeInfoWrapper.getManifest()).thenReturn(manifest3);
-        C2Heartbeat heartbeat3 = c2HeartbeatFactory.create(runtimeInfoWrapper);
-        String hash3 = heartbeat3.getAgentInfo().getAgentManifestHash();
-        assertNotEquals(hash3, hash1);
-        assertNotEquals(hash3, hash2);
+    void testAgentManifestHashIsPopulatedInCaseOfRuntimeManifest() {
+        RuntimeManifest manifest = createManifest();
+        when(manifestHashProvider.calculateManifestHash(manifest.getBundles(), Collections.emptySet())).thenReturn(MANIFEST_HASH);
+
+        C2Heartbeat heartbeat = c2HeartbeatFactory.create(new RuntimeInfoWrapper(new AgentRepositories(), manifest, new HashMap<>()));
+
+        assertEquals(MANIFEST_HASH, heartbeat.getAgentInfo().getAgentManifestHash());
+    }
+
+    @Test
+    void testAgentManifestHashIsPopulatedInCaseOfAgentManifest() {
+        AgentManifest manifest = new AgentManifest(createManifest());
+        SupportedOperation supportedOperation = new SupportedOperation();
+        supportedOperation.setType(OperationType.HEARTBEAT);
+        Set<SupportedOperation> supportedOperations = Collections.singleton(supportedOperation);
+        manifest.setSupportedOperations(supportedOperations);
+        when(manifestHashProvider.calculateManifestHash(manifest.getBundles(), supportedOperations)).thenReturn(MANIFEST_HASH);
+
+        C2Heartbeat heartbeat = c2HeartbeatFactory.create(new RuntimeInfoWrapper(new AgentRepositories(), manifest, new HashMap<>()));
+
+        assertEquals(MANIFEST_HASH, heartbeat.getAgentInfo().getAgentManifestHash());
     }
 
     private RuntimeManifest createManifest() {
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/ManifestHashProviderTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/ManifestHashProviderTest.java
new file mode 100644
index 0000000000..182423603c
--- /dev/null
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/ManifestHashProviderTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.client.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.nifi.c2.protocol.api.OperationType;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
+import org.apache.nifi.c2.protocol.component.api.Bundle;
+import org.junit.jupiter.api.Test;
+
+class ManifestHashProviderTest {
+    private ManifestHashProvider manifestHashProvider = new ManifestHashProvider();
+
+    @Test
+    void testManifestHashChangesWhenManifestBundleChanges() {
+        Bundle bundle1 = new Bundle("group1", "artifact1", "version1");
+        Bundle bundle2 = new Bundle("group2", "artifact2", "version2");
+
+        SupportedOperation supportedOperation1 = new SupportedOperation();
+        supportedOperation1.setType(OperationType.HEARTBEAT);
+        SupportedOperation supportedOperation2 = new SupportedOperation();
+        supportedOperation2.setType(OperationType.ACKNOWLEDGE);
+
+        String hash1 = manifestHashProvider.calculateManifestHash(Collections.singletonList(bundle1), Collections.singleton(supportedOperation1));
+        assertNotNull(hash1);
+
+        // same manifest should result in the same hash
+        assertEquals(hash1, manifestHashProvider.calculateManifestHash(Collections.singletonList(bundle1), Collections.singleton(supportedOperation1)));
+
+        // different manifest should result in hash change if only bundle change
+        String hash2 = manifestHashProvider.calculateManifestHash(Collections.singletonList(bundle2), Collections.singleton(supportedOperation1));
+        assertNotEquals(hash2, hash1);
+
+        // different manifest should result in hash change if only supported operation change
+        String hash3 = manifestHashProvider.calculateManifestHash(Collections.singletonList(bundle1), Collections.singleton(supportedOperation2));
+        assertNotEquals(hash3, hash1);
+
+        // different manifest with multiple bundles should result in hash change compared to all previous
+        String hash4 = manifestHashProvider.calculateManifestHash(Arrays.asList(bundle1, bundle2), Collections.singleton(supportedOperation1));
+
+        assertNotEquals(hash4, hash1);
+        assertNotEquals(hash4, hash2);
+        assertNotEquals(hash4, hash3);
+    }
+}
\ No newline at end of file
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/C2OperationServiceTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/C2OperationServiceTest.java
index 0249369e27..d821dd0738 100644
--- a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/C2OperationServiceTest.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/C2OperationServiceTest.java
@@ -16,11 +16,16 @@
  */
 package org.apache.nifi.c2.client.service.operation;
 
+import static org.apache.nifi.c2.protocol.api.OperandType.CONFIGURATION;
+import static org.apache.nifi.c2.protocol.api.OperandType.MANIFEST;
+import static org.apache.nifi.c2.protocol.api.OperationType.DESCRIBE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Map;
 import java.util.Optional;
 import org.apache.nifi.c2.protocol.api.C2Operation;
 import org.apache.nifi.c2.protocol.api.C2OperationAck;
@@ -45,7 +50,7 @@ public class C2OperationServiceTest {
 
         C2Operation operation = new C2Operation();
         operation.setOperation(OperationType.UPDATE);
-        operation.setOperand(OperandType.CONFIGURATION);
+        operation.setOperand(CONFIGURATION);
         Optional<C2OperationAck> ack = service.handleOperation(operation);
 
         assertFalse(ack.isPresent());
@@ -56,8 +61,8 @@ public class C2OperationServiceTest {
         C2OperationService service = new C2OperationService(Collections.singletonList(new TestDescribeOperationHandler()));
 
         C2Operation operation = new C2Operation();
-        operation.setOperation(OperationType.DESCRIBE);
-        operation.setOperand(OperandType.MANIFEST);
+        operation.setOperation(DESCRIBE);
+        operation.setOperand(MANIFEST);
         Optional<C2OperationAck> ack = service.handleOperation(operation);
 
         assertTrue(ack.isPresent());
@@ -69,23 +74,41 @@ public class C2OperationServiceTest {
         C2OperationService service = new C2OperationService(Collections.singletonList(new TestInvalidOperationHandler()));
 
         C2Operation operation = new C2Operation();
-        operation.setOperation(OperationType.DESCRIBE);
-        operation.setOperand(OperandType.MANIFEST);
+        operation.setOperation(DESCRIBE);
+        operation.setOperand(MANIFEST);
         Optional<C2OperationAck> ack = service.handleOperation(operation);
 
         assertFalse(ack.isPresent());
     }
 
+    @Test
+    void testHandlersAreReturned() {
+        C2OperationService service = new C2OperationService(Arrays.asList(new TestDescribeOperationHandler(), new TestInvalidOperationHandler()));
+
+        Map<OperationType, Map<OperandType, C2OperationHandler>> handlers = service.getHandlers();
+
+        assertEquals(1, handlers.keySet().size());
+        assertTrue(handlers.keySet().contains(DESCRIBE));
+        Map<OperandType, C2OperationHandler> operands = handlers.values().stream().findFirst().get();
+        assertEquals(2, operands.size());
+        assertTrue(operands.keySet().containsAll(Arrays.asList(MANIFEST, CONFIGURATION)));
+    }
+
     private static class TestDescribeOperationHandler implements C2OperationHandler {
 
         @Override
         public OperationType getOperationType() {
-            return OperationType.DESCRIBE;
+            return DESCRIBE;
         }
 
         @Override
         public OperandType getOperandType() {
-            return OperandType.MANIFEST;
+            return MANIFEST;
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            return Collections.emptyMap();
         }
 
         @Override
@@ -98,12 +121,17 @@ public class C2OperationServiceTest {
 
         @Override
         public OperationType getOperationType() {
-            return OperationType.DESCRIBE;
+            return DESCRIBE;
         }
 
         @Override
         public OperandType getOperandType() {
-            return OperandType.CONFIGURATION;
+            return CONFIGURATION;
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            return Collections.emptyMap();
         }
 
         @Override
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandlerTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandlerTest.java
index c28baee247..31ff4cf35b 100644
--- a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandlerTest.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DebugOperationHandlerTest.java
@@ -79,6 +79,9 @@ public class DebugOperationHandlerTest {
     @Mock
     private C2Client c2Client;
 
+    @Mock
+    private OperandPropertiesProvider operandPropertiesProvider;
+
     @TempDir
     private File tempDir;
 
@@ -96,13 +99,13 @@ public class DebugOperationHandlerTest {
     @ParameterizedTest(name = "c2Client={0} bundleFileList={1} contentFilter={2}")
     @MethodSource("invalidConstructorArguments")
     public void testAttemptingCreateWithInvalidParametersWillThrowException(C2Client c2Client, List<Path> bundleFilePaths, Predicate<String> contentFilter) {
-        assertThrows(IllegalArgumentException.class, () -> DebugOperationHandler.create(c2Client, bundleFilePaths, contentFilter));
+        assertThrows(IllegalArgumentException.class, () -> DebugOperationHandler.create(c2Client, bundleFilePaths, contentFilter, operandPropertiesProvider));
     }
 
     @Test
     public void testOperationAndOperandTypesAreMatching() {
         // given
-        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, VALID_BUNDLE_FILE_LIST, DEFAULT_CONTENT_FILTER);
+        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, VALID_BUNDLE_FILE_LIST, DEFAULT_CONTENT_FILTER, operandPropertiesProvider);
 
         // when + then
         assertEquals(TRANSFER, testHandler.getOperationType());
@@ -112,7 +115,7 @@ public class DebugOperationHandlerTest {
     @Test
     public void testC2CallbackUrlIsNullInArgs() {
         // given
-        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, VALID_BUNDLE_FILE_LIST, DEFAULT_CONTENT_FILTER);
+        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, VALID_BUNDLE_FILE_LIST, DEFAULT_CONTENT_FILTER, operandPropertiesProvider);
         C2Operation c2Operation = operation(null);
 
         // when
@@ -131,7 +134,7 @@ public class DebugOperationHandlerTest {
         List<Path> createBundleFiles = bundleFileNamesWithContents.entrySet().stream()
             .map(entry -> placeFileWithContent(entry.getKey(), entry.getValue()))
             .collect(toList());
-        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, createBundleFiles, DEFAULT_CONTENT_FILTER);
+        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, createBundleFiles, DEFAULT_CONTENT_FILTER, operandPropertiesProvider);
         C2Operation c2Operation = operation(C2_DEBUG_UPLOAD_ENDPOINT);
 
         // when
@@ -151,7 +154,8 @@ public class DebugOperationHandlerTest {
     @Test
     public void testFileToCollectDoesNotExist() {
         // given
-        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, singletonList(Paths.get(tempDir.getAbsolutePath(), "missing_file")), DEFAULT_CONTENT_FILTER);
+        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, singletonList(Paths.get(tempDir.getAbsolutePath(), "missing_file")),
+            DEFAULT_CONTENT_FILTER, operandPropertiesProvider);
         C2Operation c2Operation = operation(C2_DEBUG_UPLOAD_ENDPOINT);
 
         // when
@@ -190,7 +194,7 @@ public class DebugOperationHandlerTest {
         // given
         Path bundleFile = placeFileWithContent(fileName, inputContent);
         Predicate<String> testContentFilter = content -> !content.contains(filterKeyword);
-        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, singletonList(bundleFile), testContentFilter);
+        DebugOperationHandler testHandler = DebugOperationHandler.create(c2Client, singletonList(bundleFile), testContentFilter, operandPropertiesProvider);
         C2Operation c2Operation = operation(C2_DEBUG_UPLOAD_ENDPOINT);
 
         // when
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandlerTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandlerTest.java
index 2017cc18a5..f4508e7fed 100644
--- a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandlerTest.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/DescribeManifestOperationHandlerTest.java
@@ -43,10 +43,12 @@ public class DescribeManifestOperationHandlerTest {
 
     @Mock
     private C2HeartbeatFactory heartbeatFactory;
+    @Mock
+    private OperandPropertiesProvider operandPropertiesProvider;
 
     @Test
     void testDescribeManifestOperationHandlerCreateSuccess() {
-        DescribeManifestOperationHandler handler = new DescribeManifestOperationHandler(null, null);
+        DescribeManifestOperationHandler handler = new DescribeManifestOperationHandler(null, null, operandPropertiesProvider);
 
         assertEquals(OperationType.DESCRIBE, handler.getOperationType());
         assertEquals(OperandType.MANIFEST, handler.getOperandType());
@@ -67,7 +69,7 @@ public class DescribeManifestOperationHandlerTest {
         heartbeat.setFlowInfo(flowInfo);
 
         when(heartbeatFactory.create(runtimeInfoWrapper)).thenReturn(heartbeat);
-        DescribeManifestOperationHandler handler = new DescribeManifestOperationHandler(heartbeatFactory, () -> runtimeInfoWrapper);
+        DescribeManifestOperationHandler handler = new DescribeManifestOperationHandler(heartbeatFactory, () -> runtimeInfoWrapper, operandPropertiesProvider);
 
         C2Operation operation = new C2Operation();
         operation.setIdentifier(OPERATION_ID);
diff --git a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProviderTest.java
similarity index 50%
copy from c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
copy to c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProviderTest.java
index e9f2db29fb..9569838b7f 100644
--- a/c2/c2-client-bundle/c2-client-service/src/main/java/org/apache/nifi/c2/client/service/operation/C2OperationHandler.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/EmptyOperandPropertiesProviderTest.java
@@ -14,37 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.nifi.c2.client.service.operation;
 
-import org.apache.nifi.c2.protocol.api.C2Operation;
-import org.apache.nifi.c2.protocol.api.C2OperationAck;
-import org.apache.nifi.c2.protocol.api.OperandType;
-import org.apache.nifi.c2.protocol.api.OperationType;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
-/**
- * Handler interface for the different operation types
- */
-public interface C2OperationHandler {
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
 
-    /**
-     * Returns the supported OperationType by the handler
-     *
-     * @return the type of the operation
-     */
-    OperationType getOperationType();
+class EmptyOperandPropertiesProviderTest {
 
-    /**
-     * Returns the supported OperandType by the handler
-     *
-     * @return the type of the operand
-     */
-    OperandType getOperandType();
+    private final OperandPropertiesProvider operandPropertiesProvider = new EmptyOperandPropertiesProvider();
 
-    /**
-     * Handler logic for the specific C2Operation
-     *
-     * @param operation the C2Operation to be handled
-     * @return the result of the operation handling
-     */
-    C2OperationAck handle(C2Operation operation);
-}
+    @Test
+    void testEmptyMapReturn() {
+        assertEquals(Collections.emptyMap(), operandPropertiesProvider.getProperties());
+    }
+}
\ No newline at end of file
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProviderTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProviderTest.java
new file mode 100644
index 0000000000..12d0a1b4ae
--- /dev/null
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/SupportedOperationsProviderTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.client.service.operation;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.nifi.c2.protocol.api.OperandType;
+import org.apache.nifi.c2.protocol.api.OperationType;
+import org.apache.nifi.c2.protocol.api.SupportedOperation;
+import org.junit.jupiter.api.Test;
+
+class SupportedOperationsProviderTest {
+
+    @Test
+    void testSupportedOperationsAreProvided() {
+        C2OperationHandler describeManifestOperationHandler = mock(C2OperationHandler.class);
+        C2OperationHandler describeConfigurationOperationHandler = mock(C2OperationHandler.class);
+        Map<String, Object> describeManifestProperties = Collections.singletonMap("availableProperties", Arrays.asList("property1", "property2"));
+        Map<String, Object> describeConfigurationProperties = Collections.emptyMap();
+        when(describeManifestOperationHandler.getProperties()).thenReturn(describeManifestProperties);
+        when(describeManifestOperationHandler.getOperandType()).thenReturn(OperandType.MANIFEST);
+        when(describeConfigurationOperationHandler.getProperties()).thenReturn(describeConfigurationProperties);
+        when(describeConfigurationOperationHandler.getOperandType()).thenReturn(OperandType.CONFIGURATION);
+
+        Map<OperationType, Map<OperandType, C2OperationHandler>> operationHandlers = new HashMap<>();
+        operationHandlers.put(OperationType.PAUSE, Collections.emptyMap());
+        Map<OperandType, C2OperationHandler> operandHandlers = new HashMap<>();
+        operandHandlers.put(OperandType.MANIFEST, describeManifestOperationHandler);
+        operandHandlers.put(OperandType.CONFIGURATION, describeConfigurationOperationHandler);
+        operationHandlers.put(OperationType.DESCRIBE, operandHandlers);
+
+        SupportedOperationsProvider supportedOperationsProvider = new SupportedOperationsProvider(operationHandlers);
+
+        SupportedOperation pauseOperation = new SupportedOperation();
+        pauseOperation.setType(OperationType.PAUSE);
+        pauseOperation.setProperties(Collections.emptyMap());
+        SupportedOperation describeOperation = new SupportedOperation();
+        describeOperation.setType(OperationType.DESCRIBE);
+        Map<OperandType, Map<String, Object>> operands = new HashMap<>();
+        operands.put(OperandType.MANIFEST, describeManifestProperties);
+        operands.put(OperandType.CONFIGURATION, describeConfigurationProperties);
+        describeOperation.setProperties(operands);
+
+        Set<SupportedOperation> expected = new HashSet<>(Arrays.asList(pauseOperation, describeOperation));
+        assertEquals(expected, supportedOperationsProvider.getSupportedOperations());
+    }
+}
\ No newline at end of file
diff --git a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandlerTest.java b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandlerTest.java
index a37fc64998..11acc072dc 100644
--- a/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandlerTest.java
+++ b/c2/c2-client-bundle/c2-client-service/src/test/java/org/apache/nifi/c2/client/service/operation/UpdateConfigurationOperationHandlerTest.java
@@ -17,8 +17,8 @@
 package org.apache.nifi.c2.client.service.operation;
 
 import static org.apache.commons.lang3.StringUtils.EMPTY;
-import static org.apache.nifi.c2.client.service.operation.UpdateConfigurationOperationHandler.LOCATION;
 import static org.apache.nifi.c2.client.service.operation.UpdateConfigurationOperationHandler.FLOW_ID;
+import static org.apache.nifi.c2.client.service.operation.UpdateConfigurationOperationHandler.LOCATION;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
@@ -51,10 +51,12 @@ public class UpdateConfigurationOperationHandlerTest {
 
     @Mock
     private FlowIdHolder flowIdHolder;
+    @Mock
+    private OperandPropertiesProvider operandPropertiesProvider;
 
     @Test
     void testUpdateConfigurationOperationHandlerCreateSuccess() {
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(null, null, null);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(null, null, null, operandPropertiesProvider);
 
         assertEquals(OperationType.UPDATE, handler.getOperationType());
         assertEquals(OperandType.CONFIGURATION, handler.getOperandType());
@@ -62,7 +64,7 @@ public class UpdateConfigurationOperationHandlerTest {
 
     @Test
     void testHandleIncorrectArg() {
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(null, null, null);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(null, null, null, operandPropertiesProvider);
         C2Operation operation = new C2Operation();
         operation.setArgs(INCORRECT_LOCATION_MAP);
 
@@ -76,7 +78,7 @@ public class UpdateConfigurationOperationHandlerTest {
         Function<byte[], Boolean> successUpdate = x -> true;
         when(flowIdHolder.getFlowId()).thenReturn(FLOW_ID);
         when(client.retrieveUpdateContent(any())).thenReturn(Optional.of("content".getBytes()));
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, successUpdate);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, successUpdate, operandPropertiesProvider);
         C2Operation operation = new C2Operation();
         operation.setIdentifier(OPERATION_ID);
 
@@ -95,7 +97,7 @@ public class UpdateConfigurationOperationHandlerTest {
     void testHandleReturnsNotAppliedWithNoContent() {
         when(flowIdHolder.getFlowId()).thenReturn(FLOW_ID);
         when(client.retrieveUpdateContent(any())).thenReturn(Optional.empty());
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, null);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, null, operandPropertiesProvider);
         C2Operation operation = new C2Operation();
         operation.setArgs(CORRECT_LOCATION_MAP);
 
@@ -110,7 +112,7 @@ public class UpdateConfigurationOperationHandlerTest {
         Function<byte[], Boolean> failedToUpdate = x -> false;
         when(flowIdHolder.getFlowId()).thenReturn(FLOW_ID);
         when(client.retrieveUpdateContent(any())).thenReturn(Optional.of("content".getBytes()));
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, failedToUpdate);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, failedToUpdate, operandPropertiesProvider);
         C2Operation operation = new C2Operation();
         operation.setIdentifier(OPERATION_ID);
         operation.setArgs(CORRECT_LOCATION_MAP);
@@ -126,7 +128,7 @@ public class UpdateConfigurationOperationHandlerTest {
         Function<byte[], Boolean> successUpdate = x -> true;
         when(flowIdHolder.getFlowId()).thenReturn(FLOW_ID);
         when(client.retrieveUpdateContent(any())).thenReturn(Optional.of("content".getBytes()));
-        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, successUpdate);
+        UpdateConfigurationOperationHandler handler = new UpdateConfigurationOperationHandler(client, flowIdHolder, successUpdate, operandPropertiesProvider);
         C2Operation operation = new C2Operation();
         operation.setIdentifier(OPERATION_ID);
         operation.setArgs(CORRECT_LOCATION_MAP);
diff --git a/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/AgentManifest.java b/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/AgentManifest.java
new file mode 100644
index 0000000000..3263fd3889
--- /dev/null
+++ b/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/AgentManifest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.protocol.api;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.nifi.c2.protocol.component.api.RuntimeManifest;
+
+@ApiModel
+public class AgentManifest extends RuntimeManifest {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("All supported operations by agent")
+    private Set<SupportedOperation> supportedOperations;
+
+    public AgentManifest() {
+        super();
+    }
+
+    public AgentManifest(RuntimeManifest manifest) {
+        super();
+        setAgentType(manifest.getAgentType());
+        setIdentifier(manifest.getIdentifier());
+        setBundles(manifest.getBundles());
+        setBuildInfo(manifest.getBuildInfo());
+        setSchedulingDefaults(manifest.getSchedulingDefaults());
+        setVersion(manifest.getVersion());
+    }
+
+    public Set<SupportedOperation> getSupportedOperations() {
+        return supportedOperations;
+    }
+
+    public void setSupportedOperations(Set<SupportedOperation> supportedOperations) {
+        this.supportedOperations = supportedOperations;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+        AgentManifest that = (AgentManifest) o;
+        return Objects.equals(supportedOperations, that.supportedOperations);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), supportedOperations);
+    }
+
+
+    @Override
+    public String toString() {
+        return "AgentManifest{" +
+            "supportedOperations=" + supportedOperations +
+            "}, " + super.toString();
+    }
+}
diff --git a/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/SupportedOperation.java b/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/SupportedOperation.java
new file mode 100644
index 0000000000..ede22079a6
--- /dev/null
+++ b/c2/c2-protocol/c2-protocol-api/src/main/java/org/apache/nifi/c2/protocol/api/SupportedOperation.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nifi.c2.protocol.api;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Objects;
+
+@ApiModel
+public class SupportedOperation implements Serializable {
+    private static final long serialVersionUID = 1;
+
+    @ApiModelProperty("The type of the operation supported by the agent")
+    private OperationType type;
+
+    @ApiModelProperty("Operand specific properties defined by the agent")
+    private Map<OperandType, Map<String, Object>> properties;
+
+    public OperationType getType() {
+        return type;
+    }
+
+    public void setType(OperationType type) {
+        this.type = type;
+    }
+
+    public Map<OperandType, Map<String, Object>> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Map<OperandType, Map<String, Object>> properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        SupportedOperation that = (SupportedOperation) o;
+        return type == that.type && Objects.equals(properties, that.properties);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, properties);
+    }
+
+    @Override
+    public String toString() {
+        return "SupportedOperation{" +
+            "type=" + type +
+            ", properties=" + properties +
+            '}';
+    }
+}
diff --git a/minifi/minifi-docs/src/main/markdown/minifi-java-agent-quick-start.md b/minifi/minifi-docs/src/main/markdown/minifi-java-agent-quick-start.md
index 3a24adf096..126a391634 100644
--- a/minifi/minifi-docs/src/main/markdown/minifi-java-agent-quick-start.md
+++ b/minifi/minifi-docs/src/main/markdown/minifi-java-agent-quick-start.md
@@ -152,6 +152,21 @@ To load a new dataflow for a MiNiFi instance to run:
 1. Change the flow definition on the C2 Server
 2. When a new flow is available on the C2 server, MiNiFi will download it via C2 and restart itself to pick up the changes
 
+## C2 Heartbeat
+Heartbeat provides status(agent, flowm device) and operational capabilities to C2 server(s)
+
+### Agent manifest
+The agent manifest is the descriptor of the available extensions. The size of the heartbeat
+might increase depending on the added extensions.
+
+With the `c2.full.heartbeat` parameter you can control whether to always include the manifest in the heartbeat or not.
+
+The `agentInfo.agentManifestHash` node can be used to detect in the C2 server whether the manifest changed compared to the previous heartbeat.
+
+If change is detected, a full heartbeat can be retrieved by sending a DESCRIBE MANIFEST Operation in the `requestedOperations` node of the C2 Heartbeat response.
+
+For more details about the C2 protocol please visit [Apache NiFi - MiNiFi C2 wiki page](https://cwiki.apache.org/confluence/display/MINIFI/C2).
+
 ## Using Processors Not Packaged with MiNiFi
 MiNiFi is able to use the following processors out of the box:
 * UpdateAttribute
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/c2/C2NifiClientService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/c2/C2NifiClientService.java
index 41db531ef8..0602a13f3e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/c2/C2NifiClientService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/c2/C2NifiClientService.java
@@ -45,11 +45,16 @@ import org.apache.nifi.c2.client.http.C2HttpClient;
 import org.apache.nifi.c2.client.service.C2ClientService;
 import org.apache.nifi.c2.client.service.C2HeartbeatFactory;
 import org.apache.nifi.c2.client.service.FlowIdHolder;
+import org.apache.nifi.c2.client.service.ManifestHashProvider;
 import org.apache.nifi.c2.client.service.model.RuntimeInfoWrapper;
 import org.apache.nifi.c2.client.service.operation.C2OperationService;
 import org.apache.nifi.c2.client.service.operation.DebugOperationHandler;
 import org.apache.nifi.c2.client.service.operation.DescribeManifestOperationHandler;
+import org.apache.nifi.c2.client.service.operation.EmptyOperandPropertiesProvider;
+import org.apache.nifi.c2.client.service.operation.OperandPropertiesProvider;
+import org.apache.nifi.c2.client.service.operation.SupportedOperationsProvider;
 import org.apache.nifi.c2.client.service.operation.UpdateConfigurationOperationHandler;
+import org.apache.nifi.c2.protocol.api.AgentManifest;
 import org.apache.nifi.c2.protocol.api.AgentRepositories;
 import org.apache.nifi.c2.protocol.api.AgentRepositoryStatus;
 import org.apache.nifi.c2.protocol.api.FlowQueueStatus;
@@ -103,6 +108,8 @@ public class C2NifiClientService {
     private final ExtensionManifestParser extensionManifestParser = new JAXBExtensionManifestParser();
 
     private final RuntimeManifestService runtimeManifestService;
+
+    private final SupportedOperationsProvider supportedOperationsProvider;
     private final long heartbeatPeriod;
 
     public C2NifiClientService(final NiFiProperties niFiProperties, final FlowController flowController) {
@@ -118,16 +125,19 @@ public class C2NifiClientService {
         this.heartbeatPeriod = clientConfig.getHeartbeatPeriod();
         this.flowController = flowController;
         C2HttpClient client = new C2HttpClient(clientConfig, new C2JacksonSerializer());
-        C2HeartbeatFactory heartbeatFactory = new C2HeartbeatFactory(clientConfig, flowIdHolder);
+        C2HeartbeatFactory heartbeatFactory = new C2HeartbeatFactory(clientConfig, flowIdHolder, new ManifestHashProvider());
+        OperandPropertiesProvider emptyOperandPropertiesProvider = new EmptyOperandPropertiesProvider();
+        C2OperationService c2OperationService = new C2OperationService(Arrays.asList(
+            new UpdateConfigurationOperationHandler(client, flowIdHolder, this::updateFlowContent, emptyOperandPropertiesProvider),
+            new DescribeManifestOperationHandler(heartbeatFactory, this::generateRuntimeInfo, emptyOperandPropertiesProvider),
+            DebugOperationHandler.create(client, debugBundleFiles(niFiProperties), EXCLUDE_SENSITIVE_TEXT, emptyOperandPropertiesProvider)
+        ));
         this.c2ClientService = new C2ClientService(
             client,
             heartbeatFactory,
-            new C2OperationService(Arrays.asList(
-                new UpdateConfigurationOperationHandler(client, flowIdHolder, this::updateFlowContent),
-                new DescribeManifestOperationHandler(heartbeatFactory, this::generateRuntimeInfo),
-                DebugOperationHandler.create(client, debugBundleFiles(niFiProperties), EXCLUDE_SENSITIVE_TEXT)
-            ))
+            c2OperationService
         );
+        this.supportedOperationsProvider = new SupportedOperationsProvider(c2OperationService.getHandlers());
     }
 
     private C2ClientConfig generateClientConfig(NiFiProperties properties) {
@@ -159,12 +169,7 @@ public class C2NifiClientService {
     }
 
     public void start() {
-        try {
-            scheduledExecutorService.scheduleAtFixedRate(() -> c2ClientService.sendHeartbeat(generateRuntimeInfo()), INITIAL_DELAY, heartbeatPeriod, TimeUnit.MILLISECONDS);
-        } catch (Exception e) {
-            logger.error("Could not start C2 Client Heartbeat Reporting", e);
-            throw new RuntimeException(e);
-        }
+        scheduledExecutorService.scheduleAtFixedRate(() -> c2ClientService.sendHeartbeat(generateRuntimeInfo()), INITIAL_DELAY, heartbeatPeriod, TimeUnit.MILLISECONDS);
     }
 
     public void stop() {
@@ -177,7 +182,9 @@ public class C2NifiClientService {
     }
 
     private RuntimeInfoWrapper generateRuntimeInfo() {
-        return new RuntimeInfoWrapper(getAgentRepositories(), runtimeManifestService.getManifest(), getQueueStatus());
+        AgentManifest agentManifest = new AgentManifest(runtimeManifestService.getManifest());
+        agentManifest.setSupportedOperations(supportedOperationsProvider.getSupportedOperations());
+        return new RuntimeInfoWrapper(getAgentRepositories(), agentManifest, getQueueStatus());
     }
 
     private AgentRepositories getAgentRepositories() {