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

pauloricardomg pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new b4fa381c CASSSIDECAR-429: Add RFC 6902-inspired JSON Patch support to 
ConfigurationManager (#369)
b4fa381c is described below

commit b4fa381c39b26ba57c7cc4c10f89b17b7560b5f3
Author: Paulo Motta <[email protected]>
AuthorDate: Tue Aug 11 17:35:17 2026 -0300

    CASSSIDECAR-429: Add RFC 6902-inspired JSON Patch support to 
ConfigurationManager (#369)
    
    Patch by Paulo Motta; Reviewed by Francisco Guerrero, Shailaja Koppu for 
CASSSIDECAR-429
---
 CHANGES.txt                                        |   1 +
 .../CassandraConfigurationOverlay.java             |  71 +--
 .../sidecar/configmanagement/ConfigUtils.java      |  38 ++
 .../ConfigurationConflictException.java            |  54 ++
 .../configmanagement/ConfigurationManager.java     | 103 +++-
 .../ConfigurationPatchApplier.java                 | 465 ++++++++++++++
 .../ConfigurationPatchException.java               |  45 ++
 .../ConfigurationPatchOperation.java               | 117 ++++
 .../ConfigurationPatchValidator.java               | 300 +++++++++
 .../configmanagement/ConfigurationProvider.java    |   6 +-
 .../CassandraConfigurationOverlayTest.java         | 116 +---
 .../configmanagement/ConfigurationManagerTest.java | 466 ++++++++++++++
 .../ConfigurationPatchApplierTest.java             | 667 +++++++++++++++++++++
 .../ConfigurationPatchValidatorTest.java           | 437 ++++++++++++++
 14 files changed, 2715 insertions(+), 171 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 6c43ac33..d440bcea 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.5.0
 -----
+ * Add RFC 6902-inspired JSON Patch support to ConfigurationManager 
(CASSSIDECAR-429)
  * Implement job coordination for cluster-wide operations (CASSSIDECAR-377)
  * Fix dead/dropped CDC lifecycle metrics and duplicate SidecarCdcStats 
interface (CASSSIDECAR-489)
  * Wire CDC configs in configs table to 
SidecarCdcOptions/SidecarStatePersister (CASSSIDECAR-483)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlay.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlay.java
index ad518b90..57a06137 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlay.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlay.java
@@ -39,6 +39,9 @@ import org.jetbrains.annotations.Nullable;
  * <p>The {@code extraJvmOpts} field contains JVM options that are appended to 
the Cassandra JVM startup
  * command. Each entry maps the full option flag (e.g. {@code 
-Dcassandra.jmx.local.port}) to its value
  * (e.g. {@code 7199}). These are opaque strings not subject to schema 
validation.
+ *
+ * <p>This class is immutable. Mutations are performed by {@link 
ConfigurationPatchApplier} which
+ * constructs a new instance with the desired changes.
  */
 public class CassandraConfigurationOverlay
 {
@@ -59,7 +62,7 @@ public class CassandraConfigurationOverlay
 
     /**
      * Returns the cassandra.yaml overlay as a version-agnostic JSON object. 
Callers must not mutate the
-     * returned object; use {@link #updated} to produce a new overlay with 
changes applied.
+     * returned object; use {@link ConfigurationPatchApplier} to produce a new 
overlay with changes applied.
      *
      * @return the cassandra.yaml overlay as a version-agnostic JSON object
      */
@@ -114,59 +117,6 @@ public class CassandraConfigurationOverlay
         return new CassandraConfigurationOverlay(cassandraYaml, extraJvmOpts);
     }
 
-    /**
-     * Returns a new overlay with the given updates applied. The current 
instance is not modified.
-     *
-     * <p>Both parameters follow the same semantics: a {@code null} value for 
a key removes that entry,
-     * a non-null value upserts it.
-     *
-     * @param cassandraYamlUpdates field-level changes to cassandra.yaml: key 
= field name, value = new value.
-     *                             A {@code null} value removes the field. 
Pass {@code null} for no yaml changes.
-     * @param extraJvmOptsUpdates  JVM option changes: key = option name, 
value = new option value.
-     *                             A {@code null} value removes the option. 
Pass {@code null} for no changes.
-     * @return a new overlay with the updates applied
-     */
-    @NotNull
-    public CassandraConfigurationOverlay updated(@Nullable Map<String, Object> 
cassandraYamlUpdates,
-                                                 @Nullable Map<String, String> 
extraJvmOptsUpdates)
-    {
-        JsonObject mergedYaml = cassandraYaml.copy();
-        if (cassandraYamlUpdates != null)
-        {
-            for (Map.Entry<String, Object> entry : 
cassandraYamlUpdates.entrySet())
-            {
-                if (entry.getValue() == null)
-                {
-                    mergedYaml.remove(entry.getKey());
-                }
-                else
-                {
-                    mergedYaml.put(entry.getKey(), entry.getValue());
-                }
-            }
-        }
-
-        LinkedHashMap<String, String> mergedOpts = new 
LinkedHashMap<>(extraJvmOpts);
-        if (extraJvmOptsUpdates != null)
-        {
-            for (Map.Entry<String, String> entry : 
extraJvmOptsUpdates.entrySet())
-            {
-                if (entry.getValue() == null)
-                {
-                    mergedOpts.remove(entry.getKey());
-                }
-                else
-                {
-                    mergedOpts.put(entry.getKey(), entry.getValue());
-                }
-            }
-        }
-
-        validateNoConflictingBooleanOpts(mergedOpts);
-
-        return new CassandraConfigurationOverlay(mergedYaml, mergedOpts);
-    }
-
     static boolean hasConflictingBooleanOpt(Map<String, String> existing, 
String key)
     {
         String conflicting = conflictingBooleanOpt(key);
@@ -186,19 +136,6 @@ public class CassandraConfigurationOverlay
         return null;
     }
 
-    private static void validateNoConflictingBooleanOpts(Map<String, String> 
opts)
-    {
-        for (String key : opts.keySet())
-        {
-            if (hasConflictingBooleanOpt(opts, key))
-            {
-                String option = key.substring(5);
-                throw new IllegalArgumentException(
-                    "Conflicting boolean JVM options: -XX:+" + option + " and 
-XX:-" + option);
-            }
-        }
-    }
-
     @Override
     public boolean equals(Object o)
     {
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigUtils.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigUtils.java
index f511c70e..5762e7d6 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigUtils.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigUtils.java
@@ -24,9 +24,13 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
@@ -40,6 +44,8 @@ import org.jetbrains.annotations.Nullable;
  */
 public final class ConfigUtils
 {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(ConfigUtils.class);
+
     static final YAMLFactory YAML_FACTORY = new YAMLFactory()
             .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
 
@@ -125,4 +131,36 @@ public final class ConfigUtils
         result.mergeIn(overlay, true);
         return result;
     }
+
+    /**
+     * Merges overlay JVM options onto base JVM options, with overlay entries 
overriding base entries
+     * on key conflict. When an overlay entry conflicts with a base boolean 
option (e.g. base
+     * {@code -XX:+UseG1GC} and overlay {@code -XX:-UseG1GC}), the base option 
is preserved and the
+     * overlay entry is skipped, matching {@link 
ConfigurationOverlaySnapshot#overlay}. Insertion order
+     * is preserved. Neither input map is modified.
+     *
+     * @param base    the base JVM options
+     * @param overlay the overlay JVM options whose values take precedence
+     * @return a new map with the merged result
+     */
+    @NotNull
+    public static Map<String, String> mergeOpts(@NotNull Map<String, String> 
base,
+                                                @NotNull Map<String, String> 
overlay)
+    {
+        Objects.requireNonNull(base, "base must not be null");
+        Objects.requireNonNull(overlay, "overlay must not be null");
+        Map<String, String> merged = new LinkedHashMap<>(base);
+        for (Map.Entry<String, String> entry : overlay.entrySet())
+        {
+            if (CassandraConfigurationOverlay.hasConflictingBooleanOpt(merged, 
entry.getKey()))
+            {
+                LOGGER.warn("Conflicting boolean JVM option '{}' in overlay 
conflicts with base option '{}'. " +
+                            "Preserving base option and skipping overlay 
entry.",
+                            entry.getKey(), 
CassandraConfigurationOverlay.conflictingBooleanOpt(entry.getKey()));
+                continue; // preserve base boolean option, skip conflicting 
overlay entry
+            }
+            merged.put(entry.getKey(), entry.getValue());
+        }
+        return merged;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationConflictException.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationConflictException.java
new file mode 100644
index 00000000..beeb701a
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationConflictException.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.cassandra.sidecar.configmanagement;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Thrown when a configuration patch fails due to a hash mismatch,
+ * indicating a concurrent modification to the effective configuration.
+ */
+public class ConfigurationConflictException extends 
ConfigurationManagerException
+{
+    @NotNull
+    private final String expectedHash;
+
+    @NotNull
+    private final String actualHash;
+
+    public ConfigurationConflictException(@NotNull String expectedHash, 
@NotNull String actualHash)
+    {
+        super("Configuration conflict: expected hash [" + expectedHash
+              + "] but found [" + actualHash + "]", null);
+        this.expectedHash = expectedHash;
+        this.actualHash = actualHash;
+    }
+
+    @NotNull
+    public String expectedHash()
+    {
+        return expectedHash;
+    }
+
+    @NotNull
+    public String actualHash()
+    {
+        return actualHash;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManager.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManager.java
index 0ae2b6ce..5a0d6fc9 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManager.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManager.java
@@ -19,6 +19,8 @@
 package org.apache.cassandra.sidecar.configmanagement;
 
 import java.nio.file.Path;
+import java.time.Instant;
+import java.util.List;
 import java.util.Objects;
 
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
@@ -26,7 +28,11 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Manages configuration of Cassandra instances via Sidecar
+ * Manages configuration of Cassandra instances via Sidecar.
+ *
+ * <p>The <em>effective configuration</em> is the result of merging the base 
cassandra.yaml template
+ * with the instance overlay retrieved from the {@link ConfigurationProvider}. 
It is what the instance
+ * actually sees; the overlay is only the delta persisted on top of the base 
template.
  */
 public class ConfigurationManager
 {
@@ -48,8 +54,7 @@ public class ConfigurationManager
     }
 
     /**
-     * Computes the effective configuration for the given instance by merging 
the base template
-     * with the overlay from the {@link ConfigurationProvider}.
+     * Computes the effective configuration for the given instance.
      *
      * @param instance the Cassandra instance metadata
      * @return a snapshot of the effective configuration with its SHA-256 hash 
and last modified timestamp
@@ -84,4 +89,96 @@ public class ConfigurationManager
         cachedBaseSnapshot = snapshot;
         return snapshot;
     }
+
+    /**
+     * Patches the effective configuration for the given instance using RFC 
6902-inspired JSON Patch
+     * operations. Validates the caller's expected hash against the current 
effective configuration,
+     * validates and applies the patch operations to the overlay, persists via 
the
+     * {@link ConfigurationProvider}, and returns the new effective 
configuration.
+     *
+     * <p>Paths reference the effective configuration structure, but mutations 
target the overlay.
+     * For nested paths within a top-level {@code cassandraYaml} key,
+     * the entire top-level key value is copied from the effective config into 
the overlay before
+     * applying the leaf change (copy-siblings strategy). This ensures the 
overlay is always
+     * self-contained at the top-level key granularity. As a consequence, 
editing a nested leaf pins
+     * its sibling leaves against later base-template drift, and removing an 
overlaid leaf reverts it to
+     * the current base value; see {@link ConfigurationPatchApplier} for 
details.
+     *
+     * <p>All operations are validated atomically: either all succeed or none 
are applied.
+     *
+     * @param instance     the Cassandra instance metadata
+     * @param expectedHash the hash of the effective configuration as last 
seen by the caller
+     * @param operations   the patch operations to apply
+     * @return a snapshot of the new effective configuration with its SHA-256 
hash and last modified timestamp
+     * @throws ConfigurationConflictException if the expectedHash does not 
match the current effective hash
+     * @throws ConfigurationPatchException    if any patch operation fails 
validation or application
+     * @throws ConfigurationManagerException  if the provider fails during the 
operation
+     */
+    @NotNull
+    public ConfigurationOverlaySnapshot patchConfiguration(@NotNull 
InstanceMetadata instance,
+                                                           @NotNull String 
expectedHash,
+                                                           @NotNull 
List<ConfigurationPatchOperation> operations)
+    {
+        Objects.requireNonNull(instance, "instance must not be null");
+        Objects.requireNonNull(expectedHash, "expectedHash must not be null");
+        Objects.requireNonNull(operations, "operations must not be null");
+
+        try
+        {
+            // 1. Validate operations structure (path format, value presence, 
duplicates)
+            List<ConfigurationPatchValidator.ParsedPatchOperation> parsedOps =
+                    ConfigurationPatchValidator.validate(operations);
+
+            // 2. Read current state and validate the caller's hash matches
+            ConfigurationOverlaySnapshot baseSnapshot = getBaseSnapshot();
+            ConfigurationOverlaySnapshot currentOverlay = 
provider.getOverlay(instance);
+
+            ConfigurationOverlaySnapshot effectiveConfig = currentOverlay != 
null
+                                                           ? 
baseSnapshot.overlay(currentOverlay, instance.id())
+                                                           : baseSnapshot;
+
+            if (!expectedHash.equals(effectiveConfig.hash()))
+            {
+                throw new ConfigurationConflictException(expectedHash, 
effectiveConfig.hash());
+            }
+
+            // 3. Apply patch operations to the overlay (precondition checks + 
mutations)
+            CassandraConfigurationOverlay currentOverlayConfig = 
currentOverlay != null
+                                                                 ? 
currentOverlay.configuration()
+                                                                 : new 
CassandraConfigurationOverlay(null, null);
+            CassandraConfigurationOverlay updatedOverlay =
+                    ConfigurationPatchApplier.apply(parsedOps, 
baseSnapshot.configuration(), currentOverlayConfig);
+            ConfigurationOverlaySnapshot newOverlaySnapshot = new 
ConfigurationOverlaySnapshot(Instant.now(),
+                                                                               
                updatedOverlay);
+
+            // 4. CAS via provider — concurrent patches are resolved by the 
provider's own atomicity
+            String currentOverlayHash = currentOverlay != null ? 
currentOverlay.hash() : null;
+            boolean stored = provider.storeOverlay(instance, 
currentOverlayHash, newOverlaySnapshot);
+
+            // 5. Store rejected — re-read to determine if this is a true 
conflict or an unexpected failure
+            if (!stored)
+            {
+                ConfigurationOverlaySnapshot updatedCurrent = 
provider.getOverlay(instance);
+                ConfigurationOverlaySnapshot newEffective = updatedCurrent != 
null
+                                                            ? 
baseSnapshot.overlay(updatedCurrent, instance.id())
+                                                            : baseSnapshot;
+                if (!expectedHash.equals(newEffective.hash()))
+                {
+                    throw new ConfigurationConflictException(expectedHash, 
newEffective.hash());
+                }
+                throw new ConfigurationManagerException(
+                        "Provider rejected the overlay store unexpectedly", 
null);
+            }
+
+            return baseSnapshot.overlay(newOverlaySnapshot, instance.id());
+        }
+        catch (ConfigurationManagerException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new ConfigurationManagerException("Failed to patch 
configuration", e);
+        }
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplier.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplier.java
new file mode 100644
index 00000000..b5698fa0
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplier.java
@@ -0,0 +1,465 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import io.vertx.core.json.JsonObject;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchValidator.CASSANDRA_YAML_SECTION;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchValidator.EXTRA_JVM_OPTS_SECTION;
+
+/**
+ * Applies validated patch operations to the configuration overlay.
+ *
+ * <p>Paths reference the effective configuration (base + overlay merged),
+ * but mutations target the overlay only. For nested paths within a top-level 
{@code cassandraYaml} key,
+ * the entire top-level key value is copied from the effective config into the 
overlay before applying
+ * the leaf change (copy-siblings strategy).
+ *
+ * <p>Because copy-siblings captures a snapshot of the whole top-level block 
into the overlay, and the
+ * overlay is deep-merged over the base ({@link 
ConfigUtils#mergeConfigurations}), two behaviors follow
+ * that callers should be aware of:
+ * <ul>
+ *   <li><b>Editing a nested leaf pins its sibling leaves against base 
drift.</b> Every leaf present in
+ *       the top-level block at edit time is copied into the overlay and 
thereafter shadows the base, so
+ *       later changes to those same leaves in the base template no longer 
surface in the effective
+ *       configuration. Keys added to the base block <em>after</em> the edit 
are not pinned - the deep
+ *       merge still surfaces them.</li>
+ *   <li><b>Removing an overlaid leaf reverts it to the current base 
value.</b> The leaf is deleted from
+ *       the overlay, so the effective value falls back to whatever the base 
template currently holds,
+ *       which may differ from the value that was in effect when the overlay 
was written.</li>
+ * </ul>
+ *
+ * <p>Operations are applied sequentially (RFC 6902 section 5): each operation 
is validated and
+ * applied against the effective configuration produced by the previous 
operation, so later
+ * operations observe the effects of earlier ones. Mutations target a 
throwaway copy of the overlay;
+ * if any operation fails, the exception propagates and the copy is discarded, 
so nothing is
+ * persisted (all-or-nothing atomicity).
+ */
+public final class ConfigurationPatchApplier
+{
+    private ConfigurationPatchApplier()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Applies a list of validated patch operations and returns the updated 
overlay.
+     *
+     * <p>The effective configuration is re-derived from {@code baseConfig} 
and the evolving overlay
+     * before each operation, so an operation's preconditions are checked 
against the state left by the
+     * preceding operations rather than the initial snapshot.
+     *
+     * @param parsedOps      the validated and parsed operations
+     * @param baseConfig     the base configuration (base template) the 
overlay is applied on top of
+     * @param currentOverlay the current overlay (may be empty)
+     * @return the updated overlay with all operations applied
+     * @throws ConfigurationPatchException if any precondition check fails
+     */
+    @NotNull
+    public static CassandraConfigurationOverlay apply(
+            @NotNull List<ConfigurationPatchValidator.ParsedPatchOperation> 
parsedOps,
+            @NotNull CassandraConfigurationOverlay baseConfig,
+            @NotNull CassandraConfigurationOverlay currentOverlay)
+    {
+        JsonObject overlayYaml = currentOverlay.cassandraYaml().copy();
+        Map<String, String> overlayOpts = new 
LinkedHashMap<>(currentOverlay.extraJvmOpts());
+
+        for (ConfigurationPatchValidator.ParsedPatchOperation parsed : 
parsedOps)
+        {
+            // Re-derive the effective configuration so each op sees the 
effects of the previous ones.
+            JsonObject effectiveYaml = 
ConfigUtils.mergeConfigurations(baseConfig.cassandraYaml(), overlayYaml);
+            Map<String, String> effectiveOpts = 
ConfigUtils.mergeOpts(baseConfig.extraJvmOpts(), overlayOpts);
+
+            checkPreconditions(parsed, effectiveYaml, effectiveOpts, 
overlayYaml, overlayOpts);
+            applyMutation(parsed, effectiveYaml, overlayYaml, overlayOpts);
+        }
+
+        return new CassandraConfigurationOverlay(overlayYaml, overlayOpts);
+    }
+
+    private static void 
checkPreconditions(ConfigurationPatchValidator.ParsedPatchOperation parsed,
+                                           JsonObject effectiveYaml,
+                                           Map<String, String> effectiveOpts,
+                                           JsonObject overlayYaml,
+                                           Map<String, String> overlayOpts)
+    {
+        if (parsed.section().equals(CASSANDRA_YAML_SECTION))
+        {
+            checkYamlPreconditions(parsed, effectiveYaml, overlayYaml);
+        }
+        else if (parsed.section().equals(EXTRA_JVM_OPTS_SECTION))
+        {
+            checkJvmOptsPreconditions(parsed, effectiveOpts, overlayOpts);
+        }
+    }
+
+    private static void 
checkYamlPreconditions(ConfigurationPatchValidator.ParsedPatchOperation parsed,
+                                               JsonObject effectiveYaml,
+                                               JsonObject overlayYaml)
+    {
+        ConfigurationPatchOperation op = parsed.operation();
+
+        switch (op.op())
+        {
+            case TEST:
+                checkNestedPathTraversable(effectiveYaml, parsed);
+                Object actualValue = resolveValue(effectiveYaml, 
parsed.topLevelKey(), parsed.nestedSegments());
+                if (actualValue == null)
+                {
+                    throw new ConfigurationPatchException(
+                            "Test failed: path does not exist in effective 
config: '" + op.path() + "'", op);
+                }
+                // Both sides are Vert.x JSON types: resolveValue returns 
JsonObject/JsonArray/scalars,
+                // and op.value() is expected to be read from the request via 
JsonObject.getValue,
+                // which wraps objects/arrays the same way.
+                if (!Objects.equals(actualValue, op.value()))
+                {
+                    throw new ConfigurationPatchException(
+                            "Test failed: expected " + op.value() + " but 
found " + actualValue
+                            + " at path '" + op.path() + "'", op);
+                }
+                break;
+
+            case REPLACE:
+                checkNestedPathTraversable(effectiveYaml, parsed);
+                Object existingValue = resolveValue(effectiveYaml, 
parsed.topLevelKey(), parsed.nestedSegments());
+                if (existingValue == null)
+                {
+                    throw new ConfigurationPatchException(
+                            "Replace failed: path does not exist in effective 
config: '" + op.path() + "'", op);
+                }
+                break;
+
+            case REMOVE:
+                checkNestedPathTraversable(overlayYaml, parsed);
+                Object overlayValue = resolveValue(overlayYaml, 
parsed.topLevelKey(), parsed.nestedSegments());
+                if (overlayValue == null)
+                {
+                    throw new ConfigurationPatchException(
+                            "Remove failed: path does not exist in overlay 
(may only exist in base template): '"
+                            + op.path() + "'", op);
+                }
+                break;
+
+            case ADD:
+                if (parsed.isNested())
+                {
+                    // Parent must exist in effective config (RFC 6902)
+                    List<String> parentSegments = parsed.nestedSegments()
+                                                       .subList(0, 
parsed.nestedSegments().size() - 1);
+                    checkNestedPathTraversable(effectiveYaml, parsed);
+                    Object parent = resolveValue(effectiveYaml, 
parsed.topLevelKey(), parentSegments);
+                    if (parent == null && !parentSegments.isEmpty())
+                    {
+                        throw new ConfigurationPatchException(
+                                "Add failed: parent path does not exist in 
effective config: '" + op.path() + "'", op);
+                    }
+                    if (parent != null && !(parent instanceof JsonObject))
+                    {
+                        throw new ConfigurationPatchException(
+                                "Add failed: parent is not an object at path: 
'" + op.path() + "'", op);
+                    }
+                }
+                break;
+        }
+    }
+
+    /**
+     * Validates that the nested path can be traversed without hitting a 
non-object (e.g., an array)
+     * at an intermediate segment. Throws a clear error if a segment resolves 
to a value that
+     * cannot be traversed further.
+     */
+    private static void checkNestedPathTraversable(JsonObject yaml,
+                                                   
ConfigurationPatchValidator.ParsedPatchOperation parsed)
+    {
+        if (!parsed.isNested())
+        {
+            return;
+        }
+        ConfigurationPatchOperation op = parsed.operation();
+        Object current = yaml.getValue(parsed.topLevelKey());
+        if (current == null)
+        {
+            return; // will be caught by subsequent resolveValue null check
+        }
+
+        for (int i = 0; i < parsed.nestedSegments().size() - 1; i++)
+        {
+            JsonObject currentObj = asJsonObject(current);
+            if (currentObj == null)
+            {
+                throw new ConfigurationPatchException(
+                        "Path traversal failed: intermediate value is not an 
object at path: '"
+                        + op.path() + "'", op);
+            }
+            current = currentObj.getValue(parsed.nestedSegments().get(i));
+            if (current == null)
+            {
+                return; // will be caught by subsequent resolveValue null check
+            }
+        }
+    }
+
+    private static void 
checkJvmOptsPreconditions(ConfigurationPatchValidator.ParsedPatchOperation 
parsed,
+                                                  Map<String, String> 
effectiveOpts,
+                                                  Map<String, String> 
overlayOpts)
+    {
+        ConfigurationPatchOperation op = parsed.operation();
+        String key = parsed.topLevelKey();
+
+        switch (op.op())
+        {
+            case TEST:
+                if (!effectiveOpts.containsKey(key))
+                {
+                    throw new ConfigurationPatchException(
+                            "Test failed: key does not exist in effective 
extraJvmOpts: '" + op.path() + "'", op);
+                }
+                String actual = effectiveOpts.get(key);
+                if (!Objects.equals(actual, op.value()))
+                {
+                    throw new ConfigurationPatchException(
+                            "Test failed: expected '" + op.value() + "' but 
found '" + actual
+                            + "' at path '" + op.path() + "'", op);
+                }
+                break;
+
+            case REPLACE:
+                if (!effectiveOpts.containsKey(key))
+                {
+                    throw new ConfigurationPatchException(
+                            "Replace failed: key does not exist in effective 
extraJvmOpts: '" + op.path() + "'", op);
+                }
+                checkNoConflictingBooleanOpt(effectiveOpts, key, op);
+                break;
+
+            case REMOVE:
+                if (!overlayOpts.containsKey(key))
+                {
+                    throw new ConfigurationPatchException(
+                            "Remove failed: key does not exist in overlay 
extraJvmOpts "
+                            + "(may only exist in base template): '" + 
op.path() + "'", op);
+                }
+                break;
+
+            case ADD:
+                // extraJvmOpts is flat, so the parent always exists. Only 
guard against introducing a
+                // boolean option that conflicts with one already in the 
effective configuration
+                // (e.g. adding -XX:-UseG1GC while -XX:+UseG1GC is set by the 
base or a prior op).
+                checkNoConflictingBooleanOpt(effectiveOpts, key, op);
+                break;
+        }
+    }
+
+    private static void checkNoConflictingBooleanOpt(Map<String, String> 
effectiveOpts, String key,
+                                                     
ConfigurationPatchOperation op)
+    {
+        if 
(CassandraConfigurationOverlay.hasConflictingBooleanOpt(effectiveOpts, key))
+        {
+            String conflicting = 
CassandraConfigurationOverlay.conflictingBooleanOpt(key);
+            throw new ConfigurationPatchException(
+                    "Conflicting boolean JVM option: '" + key + "' conflicts 
with existing '"
+                    + conflicting + "' in the effective configuration", op);
+        }
+    }
+
+    private static void 
applyMutation(ConfigurationPatchValidator.ParsedPatchOperation parsed,
+                                      JsonObject effectiveYaml,
+                                      JsonObject updatedYaml,
+                                      Map<String, String> updatedOpts)
+    {
+        ConfigurationPatchOperation op = parsed.operation();
+
+        if (op.op() == ConfigurationPatchOperation.Op.TEST)
+        {
+            return; // test is read-only
+        }
+
+        if (parsed.section().equals(CASSANDRA_YAML_SECTION))
+        {
+            applyYamlMutation(parsed, effectiveYaml, updatedYaml);
+        }
+        else if (parsed.section().equals(EXTRA_JVM_OPTS_SECTION))
+        {
+            applyJvmOptsMutation(parsed, updatedOpts);
+        }
+    }
+
+    private static void 
applyYamlMutation(ConfigurationPatchValidator.ParsedPatchOperation parsed,
+                                          JsonObject effectiveYaml,
+                                          JsonObject updatedYaml)
+    {
+        ConfigurationPatchOperation op = parsed.operation();
+
+        if (!parsed.isNested())
+        {
+            // Top-level key: direct put/remove on overlay yaml
+            if (op.op() == ConfigurationPatchOperation.Op.REMOVE)
+            {
+                updatedYaml.remove(parsed.topLevelKey());
+            }
+            else
+            {
+                updatedYaml.put(parsed.topLevelKey(), op.value());
+            }
+        }
+        else if (op.op() == ConfigurationPatchOperation.Op.REMOVE)
+        {
+            // Nested remove: modify the existing overlay subtree in-place.
+            // The precondition check already verified the leaf exists in the 
overlay.
+            // updatedYaml is already a deep copy so in-place mutation is safe.
+            JsonObject topLevelObject = 
asJsonObject(updatedYaml.getValue(parsed.topLevelKey()));
+            if (topLevelObject != null)
+            {
+                removeAtPath(topLevelObject, parsed.nestedSegments());
+            }
+        }
+        else
+        {
+            // Nested add/replace: copy-siblings strategy.
+            // If a prior op in this batch already copied the top-level key 
into the overlay,
+            // operate on that copy. Otherwise, copy from effective config to 
ensure the overlay
+            // is self-contained at the top-level key granularity.
+            JsonObject topLevelObject;
+            if (updatedYaml.containsKey(parsed.topLevelKey()))
+            {
+                topLevelObject = 
asJsonObject(updatedYaml.getValue(parsed.topLevelKey()));
+            }
+            else
+            {
+                topLevelObject = 
deepCopyValue(effectiveYaml.getValue(parsed.topLevelKey()));
+            }
+            if (topLevelObject == null)
+            {
+                topLevelObject = new JsonObject();
+            }
+            setAtPath(topLevelObject, parsed.nestedSegments(), op.value());
+            updatedYaml.put(parsed.topLevelKey(), topLevelObject);
+        }
+    }
+
+    private static void 
applyJvmOptsMutation(ConfigurationPatchValidator.ParsedPatchOperation parsed,
+                                             Map<String, String> updatedOpts)
+    {
+        ConfigurationPatchOperation op = parsed.operation();
+        String key = parsed.topLevelKey();
+
+        if (op.op() == ConfigurationPatchOperation.Op.REMOVE)
+        {
+            updatedOpts.remove(key);
+        }
+        else
+        {
+            // Boolean-opt conflicts are rejected by checkJvmOptsPreconditions 
against the current
+            // effective configuration, so the mutation here is unconditional.
+            updatedOpts.put(key, String.valueOf(op.value()));
+        }
+    }
+
+    /**
+     * Resolves a value at the given path within a JsonObject.
+     *
+     * @param root           the root object to traverse
+     * @param topLevelKey    the first key to look up
+     * @param nestedSegments remaining path segments after the top-level key
+     * @return the value at the path, or {@code null} if any segment is missing
+     */
+    @Nullable
+    @SuppressWarnings("unchecked")
+    static Object resolveValue(JsonObject root, String topLevelKey, 
List<String> nestedSegments)
+    {
+        Object current = root.getValue(topLevelKey);
+        if (current == null)
+        {
+            return null;
+        }
+
+        for (String segment : nestedSegments)
+        {
+            JsonObject obj = asJsonObject(current);
+            if (obj == null)
+            {
+                return null;
+            }
+            current = obj.getValue(segment);
+            if (current == null)
+            {
+                return null;
+            }
+        }
+        return current;
+    }
+
+    private static void setAtPath(JsonObject root, List<String> segments, 
Object value)
+    {
+        JsonObject parent = root;
+        for (int i = 0; i < segments.size() - 1; i++)
+        {
+            Object child = parent.getValue(segments.get(i));
+            JsonObject childObj = asJsonObject(child);
+            if (childObj == null)
+            {
+                childObj = new JsonObject();
+                parent.put(segments.get(i), childObj);
+            }
+            parent = childObj;
+        }
+        parent.put(segments.get(segments.size() - 1), value);
+    }
+
+    private static void removeAtPath(JsonObject root, List<String> segments)
+    {
+        JsonObject parent = root;
+        for (int i = 0; i < segments.size() - 1; i++)
+        {
+            Object child = parent.getValue(segments.get(i));
+            JsonObject childObj = asJsonObject(child);
+            if (childObj == null)
+            {
+                // Unreachable: the remove precondition already verified the 
full path exists in the
+                // overlay against the current (post-previous-op) state, so 
every intermediate is present.
+                throw new IllegalStateException("Overlay path segment '" + 
segments.get(i)
+                                                + "' is missing or not an 
object during remove");
+            }
+            parent = childObj;
+        }
+        parent.remove(segments.get(segments.size() - 1));
+    }
+
+    // Values are always read via JsonObject.getValue, which wraps nested Maps 
into JsonObject,
+    // so a raw Map never reaches here; only JsonObject (or a non-object such 
as JsonArray/scalar).
+    @Nullable
+    private static JsonObject asJsonObject(@Nullable Object value)
+    {
+        return value instanceof JsonObject ? (JsonObject) value : null;
+    }
+
+    private static JsonObject deepCopyValue(@Nullable Object value)
+    {
+        return value instanceof JsonObject ? ((JsonObject) value).copy() : new 
JsonObject();
+    }
+
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchException.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchException.java
new file mode 100644
index 00000000..6a234ba9
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchException.java
@@ -0,0 +1,45 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Thrown when a configuration patch operation fails validation or application.
+ * Contains the failing operation for error reporting.
+ */
+public class ConfigurationPatchException extends ConfigurationManagerException
+{
+    @Nullable
+    private final ConfigurationPatchOperation failedOperation;
+
+    public ConfigurationPatchException(@NotNull String message,
+                                       @Nullable ConfigurationPatchOperation 
failedOperation)
+    {
+        super(message, null);
+        this.failedOperation = failedOperation;
+    }
+
+    @Nullable
+    public ConfigurationPatchOperation failedOperation()
+    {
+        return failedOperation;
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchOperation.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchOperation.java
new file mode 100644
index 00000000..581abb60
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchOperation.java
@@ -0,0 +1,117 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import java.util.Objects;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a single JSON Patch operation (RFC 6902-inspired) for 
configuration updates.
+ * Supported operations: {@code add}, {@code remove}, {@code replace}, {@code 
test}.
+ *
+ * <p>Paths use JSON Pointer syntax (RFC 6901) and reference the effective 
configuration structure.
+ * Operations are interpreted as overlay mutations:
+ * <ul>
+ *   <li>{@code add} — upsert value in overlay; parent must exist in effective 
config</li>
+ *   <li>{@code remove} — remove from overlay; fails if key only exists in 
base template</li>
+ *   <li>{@code replace} — set value in overlay; fails if key absent from 
effective config</li>
+ *   <li>{@code test} — assert effective config value matches; no mutation</li>
+ * </ul>
+ */
+public class ConfigurationPatchOperation
+{
+    /**
+     * The supported patch operations.
+     */
+    public enum Op
+    {
+        ADD,
+        REMOVE,
+        REPLACE,
+        TEST
+    }
+
+    @NotNull
+    private final Op op;
+
+    @NotNull
+    private final String path;
+
+    @Nullable
+    private final Object value;
+
+    /**
+     * @param op    the operation type
+     * @param path  the JSON Pointer path (e.g. 
"/configuration/cassandraYaml/concurrent_reads")
+     * @param value the value for add/replace/test operations; must be {@code 
null} for remove
+     */
+    public ConfigurationPatchOperation(@NotNull Op op, @NotNull String path, 
@Nullable Object value)
+    {
+        this.op = Objects.requireNonNull(op, "op must not be null");
+        this.path = Objects.requireNonNull(path, "path must not be null");
+        this.value = value;
+    }
+
+    @NotNull
+    public Op op()
+    {
+        return op;
+    }
+
+    @NotNull
+    public String path()
+    {
+        return path;
+    }
+
+    @Nullable
+    public Object value()
+    {
+        return value;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "ConfigurationPatchOperation{op=" + op + ", path='" + path + 
"', value=" + value + '}';
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o)
+        {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass())
+        {
+            return false;
+        }
+        ConfigurationPatchOperation that = (ConfigurationPatchOperation) o;
+        return op == that.op && Objects.equals(path, that.path) && 
Objects.equals(value, that.value);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(op, path, value);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidator.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidator.java
new file mode 100644
index 00000000..889d0eba
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidator.java
@@ -0,0 +1,300 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Validates a list of {@link ConfigurationPatchOperation} instances before 
application.
+ *
+ * <p>Validation includes:
+ * <ul>
+ *   <li>Path format: must start with {@code /configuration/cassandraYaml/} or
+ *       {@code /configuration/extraJvmOpts/}</li>
+ *   <li>Value presence: required for add/replace/test, must be absent for 
remove</li>
+ *   <li>Duplicate path detection: multiple mutation ops targeting the same 
path are rejected</li>
+ *   <li>Path segments split by {@code /}; empty segments are rejected</li>
+ * </ul>
+ */
+public final class ConfigurationPatchValidator
+{
+    static final String PATH_PREFIX = "/configuration/";
+    static final String CASSANDRA_YAML_SECTION = "cassandraYaml";
+    static final String EXTRA_JVM_OPTS_SECTION = "extraJvmOpts";
+    static final String CASSANDRA_YAML_PREFIX = PATH_PREFIX + 
CASSANDRA_YAML_SECTION + "/";
+    static final String EXTRA_JVM_OPTS_PREFIX = PATH_PREFIX + 
EXTRA_JVM_OPTS_SECTION + "/";
+
+    // Allows: -Dproperty.name, -Xmx, -Xss, -XX:+Flag, -XX:-Flag, -XX:Flag
+    // Rejects: -javaagent, -agentpath, -agentlib, keys with = or shell 
metacharacters
+    static final Pattern JVM_OPT_KEY_PATTERN = Pattern.compile(
+            
"^-(D[a-zA-Z][a-zA-Z0-9._-]*|X[a-z][a-zA-Z0-9]*|XX:[+-]?[a-zA-Z][a-zA-Z0-9_]*)$");
+
+    // JVM options that are rejected because they execute arbitrary commands 
or write to
+    // arbitrary filesystem paths. The value pattern permits absolute paths 
(Cassandra system
+    // properties legitimately need them), so path-bearing flags must be 
blocked by key instead.
+    static final Set<String> BLOCKED_JVM_OPTS = Set.of(
+            // Execute arbitrary commands
+            "-XX:OnOutOfMemoryError",
+            "-XX:OnError",
+            // Write to arbitrary filesystem paths
+            "-XX:ErrorFile",
+            "-XX:HeapDumpPath",
+            "-XX:LogFile",
+            "-XX:FlightRecorderOptions",
+            "-XX:StartFlightRecording",
+            "-Xloggc",
+            "-Xlog",
+            "-Xbootclasspath");
+
+    // Allows: alphanumeric, dots, colons, slashes, @, +, commas, hyphens, 
braces, brackets, quotes (max 512 chars).
+    // Quotes/braces/brackets permit JSON values. Whitespace is rejected 
because the Cassandra launcher
+    // word-splits it; shell metacharacters (;|&$`), newlines and other 
control characters are also rejected.
+    static final Pattern JVM_OPT_VALUE_PATTERN = 
Pattern.compile("^[a-zA-Z0-9._:/@+,\"{}\\[\\]-]{0,512}$");
+
+    // Matches /../ path traversal sequences (start, middle, or end of path)
+    static final Pattern PATH_TRAVERSAL_PATTERN = 
Pattern.compile("(?:^|/)\\.\\.(?:/|$)");
+
+    private ConfigurationPatchValidator()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Validates a list of patch operations and returns their parsed 
representations.
+     *
+     * @param operations the raw patch operations to validate
+     * @return the list of parsed operations with extracted section, top-level 
key, and nested segments
+     * @throws ConfigurationPatchException if any validation check fails
+     */
+    @NotNull
+    public static List<ParsedPatchOperation> validate(@NotNull 
List<ConfigurationPatchOperation> operations)
+    {
+        if (operations.isEmpty())
+        {
+            throw new ConfigurationPatchException("Patch operations list must 
not be empty", null);
+        }
+
+        Set<String> seenMutationPaths = new HashSet<>();
+        List<ParsedPatchOperation> parsed = new ArrayList<>(operations.size());
+
+        for (ConfigurationPatchOperation op : operations)
+        {
+            validateValuePresence(op);
+            ParsedPatchOperation parsedOp = parsePath(op);
+            // Only mutation ops (add/remove/replace) are checked for 
duplicates.
+            // test is read-only and may target the same path as a subsequent 
mutation.
+            if (op.op() != ConfigurationPatchOperation.Op.TEST && 
!seenMutationPaths.add(op.path()))
+            {
+                throw new ConfigurationPatchException(
+                        "Duplicate path in patch: '" + op.path() + "'", op);
+            }
+            parsed.add(parsedOp);
+        }
+        return parsed;
+    }
+
+    private static void validateValuePresence(ConfigurationPatchOperation op)
+    {
+        switch (op.op())
+        {
+            case ADD:
+            case REPLACE:
+            case TEST:
+                if (op.value() == null)
+                {
+                    throw new ConfigurationPatchException(
+                            "Operation '" + op.op() + "' requires a value", 
op);
+                }
+                break;
+            case REMOVE:
+                if (op.value() != null)
+                {
+                    throw new ConfigurationPatchException(
+                            "Operation 'REMOVE' must not have a value", op);
+                }
+                break;
+        }
+    }
+
+    private static ParsedPatchOperation parsePath(ConfigurationPatchOperation 
op)
+    {
+        String path = op.path();
+
+        if (path.startsWith(CASSANDRA_YAML_PREFIX))
+        {
+            String remaining = path.substring(CASSANDRA_YAML_PREFIX.length());
+            List<String> segments = parsePointerSegments(remaining, op);
+            if (segments.isEmpty())
+            {
+                throw new ConfigurationPatchException(
+                        "Path must specify at least one key after section: '" 
+ path + "'", op);
+            }
+            String topLevelKey = segments.get(0);
+            List<String> nestedSegments = segments.size() > 1 ? 
segments.subList(1, segments.size())
+                                                              : List.of();
+            return new ParsedPatchOperation(op, CASSANDRA_YAML_SECTION, 
topLevelKey, nestedSegments);
+        }
+        else if (path.startsWith(EXTRA_JVM_OPTS_PREFIX))
+        {
+            String remaining = path.substring(EXTRA_JVM_OPTS_PREFIX.length());
+            List<String> segments = parsePointerSegments(remaining, op);
+            if (segments.isEmpty())
+            {
+                throw new ConfigurationPatchException(
+                        "Path must specify at least one key after section: '" 
+ path + "'", op);
+            }
+            if (segments.size() > 1)
+            {
+                throw new ConfigurationPatchException(
+                        "extraJvmOpts paths must be flat (no nested segments): 
'" + path + "'", op);
+            }
+            String jvmOptKey = segments.get(0);
+            validateJvmOptKey(jvmOptKey, op);
+            if (op.value() != null)
+            {
+                validateJvmOptValue(String.valueOf(op.value()), op);
+            }
+            return new ParsedPatchOperation(op, EXTRA_JVM_OPTS_SECTION, 
jvmOptKey, List.of());
+        }
+        else
+        {
+            throw new ConfigurationPatchException(
+                    "Path must start with '/configuration/cassandraYaml/' or "
+                    + "'/configuration/extraJvmOpts/': '" + path + "'", op);
+        }
+    }
+
+    private static void validateJvmOptKey(String key, 
ConfigurationPatchOperation op)
+    {
+        if (!JVM_OPT_KEY_PATTERN.matcher(key).matches())
+        {
+            throw new ConfigurationPatchException(
+                    "Invalid JVM option key '" + key + "': must be a valid JVM 
option "
+                    + "(-Dproperty.name, -Xflag, or -XX:[+-]Flag)", op);
+        }
+        if (BLOCKED_JVM_OPTS.contains(key))
+        {
+            throw new ConfigurationPatchException(
+                    "Blocked extraJvmOpts key '" + key + "': this JVM option 
is not allowed", op);
+        }
+    }
+
+    private static void validateJvmOptValue(String value, 
ConfigurationPatchOperation op)
+    {
+        if (!JVM_OPT_VALUE_PATTERN.matcher(value).matches())
+        {
+            throw new ConfigurationPatchException(
+                    "Invalid extraJvmOpts value: contains disallowed 
characters or exceeds 512 characters", op);
+        }
+        if (PATH_TRAVERSAL_PATTERN.matcher(value).find())
+        {
+            throw new ConfigurationPatchException(
+                    "Invalid extraJvmOpts value: path traversal ('..') is not 
allowed", op);
+        }
+    }
+
+    /**
+     * Splits a path remainder into segments by {@code /}.
+     */
+    static List<String> parsePointerSegments(String pointer, 
ConfigurationPatchOperation op)
+    {
+        if (pointer.isEmpty())
+        {
+            return List.of();
+        }
+
+        String[] parts = pointer.split("/", -1);
+        List<String> segments = new ArrayList<>(parts.length);
+        for (String part : parts)
+        {
+            if (part.isEmpty())
+            {
+                throw new ConfigurationPatchException(
+                        "Path contains empty segment: '" + op.path() + "'", 
op);
+            }
+            segments.add(part);
+        }
+        return segments;
+    }
+
+    /**
+     * A validated and parsed patch operation with its path decomposed into 
section, top-level key,
+     * and optional nested segments.
+     */
+    public static class ParsedPatchOperation
+    {
+        private final ConfigurationPatchOperation operation;
+        private final String section;
+        private final String topLevelKey;
+        private final List<String> nestedSegments;
+
+        ParsedPatchOperation(@NotNull ConfigurationPatchOperation operation,
+                             @NotNull String section,
+                             @NotNull String topLevelKey,
+                             @NotNull List<String> nestedSegments)
+        {
+            this.operation = operation;
+            this.section = section;
+            this.topLevelKey = topLevelKey;
+            this.nestedSegments = nestedSegments;
+        }
+
+        @NotNull
+        public ConfigurationPatchOperation operation()
+        {
+            return operation;
+        }
+
+        /** Either "cassandraYaml" or "extraJvmOpts" */
+        @NotNull
+        public String section()
+        {
+            return section;
+        }
+
+        /** The first path segment after the section (e.g. "memtable", 
"concurrent_reads") */
+        @NotNull
+        public String topLevelKey()
+        {
+            return topLevelKey;
+        }
+
+        /**
+         * Segments after the top-level key; empty for flat paths.
+         * For example, path {@code 
/configuration/cassandraYaml/memtable/heap_pool} yields top-level key
+         * {@code memtable} and nested segments {@code [heap_pool]}.
+         */
+        @NotNull
+        public List<String> nestedSegments()
+        {
+            return nestedSegments;
+        }
+
+        public boolean isNested()
+        {
+            return !nestedSegments.isEmpty();
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationProvider.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationProvider.java
index 86fcb3b8..e639eef7 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationProvider.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationProvider.java
@@ -31,7 +31,7 @@ import org.jetbrains.annotations.Nullable;
  *
  * <p>The provider stores version-agnostic overlays and does not perform 
version-specific
  * validation or merge logic. Validation against a version-aware schema and 
computing updated
- * overlays (via {@link CassandraConfigurationOverlay#updated}) are the 
responsibility of the
+ * overlays (via {@link ConfigurationPatchApplier}) are the responsibility of 
the
  * Configuration Manager.
  */
 public interface ConfigurationProvider
@@ -50,8 +50,8 @@ public interface ConfigurationProvider
      * subject to hash-based optimistic concurrency control.
      *
      * <p>The caller is responsible for computing the new snapshot (via
-     * {@link CassandraConfigurationOverlay#updated}). The provider only 
validates
-     * the original hash against the currently stored version and persists the 
result.
+     * {@link ConfigurationPatchApplier}). The provider only validates the 
original
+     * hash against the currently stored version and persists the result.
      *
      * @param instance     the Cassandra instance metadata
      * @param originalHash the overlay hash from the previously read snapshot,
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlayTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlayTest.java
index 26f9f886..450f5b5a 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlayTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/CassandraConfigurationOverlayTest.java
@@ -18,8 +18,6 @@
 
 package org.apache.cassandra.sidecar.configmanagement;
 
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
@@ -27,7 +25,6 @@ import org.junit.jupiter.api.Test;
 import io.vertx.core.json.JsonObject;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * Tests for {@link CassandraConfigurationOverlay}
@@ -35,117 +32,40 @@ import static 
org.assertj.core.api.Assertions.assertThatThrownBy;
 class CassandraConfigurationOverlayTest
 {
     @Test
-    void testUpdatedAppliesCassandraYamlChanges()
+    void testConstructorDeepCopiesCassandraYaml()
     {
-        JsonObject yaml = new JsonObject()
-                          .put("concurrent_reads", 32)
-                          .put("memtable_flush_writers", 4)
-                          .put("storage_compatibility_mode", "CASSANDRA_4");
+        JsonObject yaml = new JsonObject().put("concurrent_reads", 32);
         CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(yaml, null);
 
-        Map<String, Object> updates = new LinkedHashMap<>();
-        updates.put("concurrent_reads", 64);
-        updates.put("storage_compatibility_mode", null);
-
-        CassandraConfigurationOverlay updated = overlay.updated(updates, null);
-
-        // concurrent_reads updated
-        
assertThat(updated.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(64);
-        // storage_compatibility_mode removed
-        
assertThat(updated.cassandraYaml().containsKey("storage_compatibility_mode")).isFalse();
-        // memtable_flush_writers preserved
-        
assertThat(updated.cassandraYaml().getInteger("memtable_flush_writers")).isEqualTo(4);
-    }
-
-    @Test
-    void testUpdatedUpsertsAndRemovesJvmOpts()
-    {
-        Map<String, String> jvmOpts = new LinkedHashMap<>();
-        jvmOpts.put("-Dcassandra.jmx.local.port", "7199");
-        jvmOpts.put("-Xmx", "4G");
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null, jvmOpts);
-
-        Map<String, String> updates = new LinkedHashMap<>();
-        updates.put("-Xmx", "8G");
-
-        CassandraConfigurationOverlay updated = overlay.updated(null, updates);
-
-        Map<String, String> expected = new LinkedHashMap<>();
-        expected.put("-Dcassandra.jmx.local.port", "7199");
-        expected.put("-Xmx", "8G");
-        assertThat(updated.extraJvmOpts()).containsExactlyEntriesOf(expected);
-    }
-
-    @Test
-    void testUpdatedRemovesJvmOptWithNullValue()
-    {
-        Map<String, String> jvmOpts = new LinkedHashMap<>();
-        jvmOpts.put("-Dcassandra.jmx.local.port", "7199");
-        jvmOpts.put("-Xmx", "4G");
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null, jvmOpts);
-
-        Map<String, String> updates = new LinkedHashMap<>();
-        updates.put("-Xmx", null);
-
-        CassandraConfigurationOverlay updated = overlay.updated(null, updates);
-
-        assertThat(updated.extraJvmOpts()).containsExactlyEntriesOf(Map.of(
-            "-Dcassandra.jmx.local.port", "7199"));
-    }
-
-    @Test
-    void testUpdatedRejectsConflictingBooleanOpts()
-    {
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null, Map.of("-XX:+UseG1GC", ""));
-
-        Map<String, String> updates = new LinkedHashMap<>();
-        updates.put("-XX:-UseG1GC", "");
-
-        assertThatThrownBy(() -> overlay.updated(null, updates))
-            .isInstanceOf(IllegalArgumentException.class)
-            .hasMessageContaining("-XX:+UseG1GC")
-            .hasMessageContaining("-XX:-UseG1GC");
-    }
-
-    @Test
-    void testUpdatedAllowsReplacingBooleanOpt()
-    {
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null, Map.of("-XX:+UseG1GC", ""));
-
-        Map<String, String> updates = new LinkedHashMap<>();
-        updates.put("-XX:+UseG1GC", null);
-        updates.put("-XX:-UseG1GC", "");
-
-        CassandraConfigurationOverlay updated = overlay.updated(null, updates);
+        yaml.put("concurrent_reads", 64);
 
-        
assertThat(updated.extraJvmOpts()).containsExactlyEntriesOf(Map.of("-XX:-UseG1GC",
 ""));
+        
assertThat(overlay.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(32);
     }
 
     @Test
-    void testConstructorDeepCopiesCassandraYaml()
+    void testFromJsonRoundTrip()
     {
-        JsonObject yaml = new JsonObject().put("concurrent_reads", 32);
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(yaml, null);
+        JsonObject yaml = new JsonObject()
+                          .put("concurrent_reads", 32)
+                          .put("commitlog_sync", "periodic");
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(yaml, Map.of("-Xmx", "4G"));
 
-        yaml.put("concurrent_reads", 64);
+        CassandraConfigurationOverlay fromJson = 
CassandraConfigurationOverlay.fromJson(overlay.toJson());
 
-        
assertThat(overlay.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(32);
+        
assertThat(fromJson.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(32);
+        
assertThat(fromJson.cassandraYaml().getString("commitlog_sync")).isEqualTo("periodic");
+        assertThat(fromJson.extraJvmOpts()).containsEntry("-Xmx", "4G");
     }
 
     @Test
-    void testUpdatedReturnsNewInstance()
+    void testEqualsAndHashCode()
     {
         JsonObject yaml = new JsonObject().put("concurrent_reads", 32);
-        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(yaml, Map.of("-Xmx", "4G"));
+        CassandraConfigurationOverlay a = new 
CassandraConfigurationOverlay(yaml, Map.of("-Xmx", "4G"));
+        CassandraConfigurationOverlay b = new 
CassandraConfigurationOverlay(yaml.copy(), Map.of("-Xmx", "4G"));
 
-        CassandraConfigurationOverlay updated = overlay.updated(
-            Collections.singletonMap("concurrent_reads", 64),
-            null);
-
-        assertThat(updated).isNotSameAs(overlay);
-        
assertThat(updated.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(64);
-        // Original is not modified by updated() — deep copy used internally
-        
assertThat(overlay.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(32);
+        assertThat(a).isEqualTo(b);
+        assertThat(a.hashCode()).isEqualTo(b.hashCode());
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManagerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManagerTest.java
index 49080915..719f6b0f 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManagerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationManagerTest.java
@@ -23,15 +23,23 @@ import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import io.vertx.core.json.JsonObject;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.jetbrains.annotations.NotNull;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -173,6 +181,464 @@ class ConfigurationManagerTest
         assertThat(second).isSameAs(first);
     }
 
+    @Test
+    void testPatchAddTopLevelKey()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+
+        ConfigurationOverlaySnapshot baseEffective = 
manager.getEffectiveConfiguration(instance);
+        String baseHash = baseEffective.hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 64));
+
+        ConfigurationOverlaySnapshot result = 
manager.patchConfiguration(instance, baseHash, ops);
+
+        
assertThat(result.configuration().cassandraYaml().getInteger("concurrent_reads")).isEqualTo(64);
+        
assertThat(result.configuration().cassandraYaml().getString("cluster_name")).isEqualTo("Test
 Cluster");
+        assertThat(result.hash()).startsWith("sha256:");
+        assertThat(result.hash()).isNotEqualTo(baseHash);
+    }
+
+    @Test
+    void testPatchAddJvmOpt()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/extraJvmOpts/-Xmx", "8g"));
+
+        ConfigurationOverlaySnapshot result = 
manager.patchConfiguration(instance, baseHash, ops);
+
+        
assertThat(result.configuration().extraJvmOpts()).containsEntry("-Xmx", "8g");
+    }
+
+    @Test
+    void testPatchConflictingBooleanJvmOptRejected()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        // Existing overlay enables G1GC; a patch that adds the opposite flag 
must be rejected outright
+        // rather than being stored and silently dropped when merged with the 
base.
+        Map<String, String> jvmOpts = new LinkedHashMap<>();
+        jvmOpts.put("-XX:+UseG1GC", "");
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(null, jvmOpts);
+        provider.storeOverlay(instance, null, new 
ConfigurationOverlaySnapshot(Instant.now(), initial));
+
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/extraJvmOpts/-XX:-UseG1GC", ""));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
effectiveHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Conflicting boolean JVM option");
+
+        // Nothing was stored: the overlay still enables G1GC and does not 
contain the conflicting flag
+        CassandraConfigurationOverlay storedOverlay = 
provider.getOverlay(instance).configuration();
+        assertThat(storedOverlay.extraJvmOpts()).containsEntry("-XX:+UseG1GC", 
"");
+        
assertThat(storedOverlay.extraJvmOpts()).doesNotContainKey("-XX:-UseG1GC");
+    }
+
+    @Test
+    void testPatchReturnedConfigMatchesStoredConfig()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        Map<String, String> jvmOpts = new LinkedHashMap<>();
+        jvmOpts.put("-XX:+UseG1GC", "");
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(null, jvmOpts);
+        provider.storeOverlay(instance, null, new 
ConfigurationOverlaySnapshot(Instant.now(), initial));
+
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/extraJvmOpts/-Xmx", "8g"));
+
+        ConfigurationOverlaySnapshot returned = 
manager.patchConfiguration(instance, effectiveHash, ops);
+
+        // The effective config returned to the caller must equal the 
effective config computed from the
+        // persisted overlay on a subsequent read (no silent divergence 
between stored and returned state).
+        ConfigurationOverlaySnapshot reread = 
manager.getEffectiveConfiguration(instance);
+        assertThat(returned.hash()).isEqualTo(reread.hash());
+        assertThat(returned.configuration()).isEqualTo(reread.configuration());
+        assertThat(returned.configuration().extraJvmOpts())
+                .containsEntry("-XX:+UseG1GC", "")
+                .containsEntry("-Xmx", "8g");
+    }
+
+    @Test
+    void testPatchRemoveTopLevelKeyFromOverlay()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        JsonObject initialYaml = new JsonObject().put("concurrent_reads", 128);
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(initialYaml, null);
+        provider.storeOverlay(instance, null, new 
ConfigurationOverlaySnapshot(Instant.now(), initial));
+
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.REMOVE,
+                                               
"/configuration/cassandraYaml/concurrent_reads", null));
+
+        ConfigurationOverlaySnapshot result = 
manager.patchConfiguration(instance, effectiveHash, ops);
+
+        // Falls back to base template value
+        
assertThat(result.configuration().cassandraYaml().getInteger("concurrent_reads")).isEqualTo(32);
+    }
+
+    @Test
+    void testPatchRemoveTemplateOnlyKeyFails()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.REMOVE,
+                                               
"/configuration/cassandraYaml/cluster_name", null));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
baseHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in overlay");
+    }
+
+    @Test
+    void testPatchReplaceExistingKey()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        // concurrent_reads=32 exists in base template
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.REPLACE,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 256));
+
+        ConfigurationOverlaySnapshot result = 
manager.patchConfiguration(instance, baseHash, ops);
+
+        
assertThat(result.configuration().cassandraYaml().getInteger("concurrent_reads")).isEqualTo(256);
+    }
+
+    @Test
+    void testPatchReplaceAbsentKeyFails()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.REPLACE,
+                                               
"/configuration/cassandraYaml/nonexistent_key", 42));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
baseHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in effective config");
+    }
+
+    @Test
+    void testPatchTestMatchingValue()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.TEST,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 32),
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        ConfigurationOverlaySnapshot result = 
manager.patchConfiguration(instance, baseHash, ops);
+
+        
assertThat(result.configuration().cassandraYaml().getInteger("concurrent_reads")).isEqualTo(128);
+    }
+
+    @Test
+    void testPatchTestMismatchRejectsEntirePatch()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        JsonObject initialYaml = new JsonObject().put("concurrent_reads", 64);
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(initialYaml, null);
+        provider.storeOverlay(instance, null, new 
ConfigurationOverlaySnapshot(Instant.now(), initial));
+
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.TEST,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 999),
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/memtable_flush_writers", 16));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
effectiveHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+
+        // Verify no mutation occurred
+        ConfigurationOverlaySnapshot current = 
manager.getEffectiveConfiguration(instance);
+        
assertThat(current.configuration().cassandraYaml().getInteger("concurrent_reads")).isEqualTo(64);
+        
assertThat(current.configuration().cassandraYaml().containsKey("memtable_flush_writers")).isFalse();
+    }
+
+    @Test
+    void testPatchConflictStaleHash()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        JsonObject initialYaml = new JsonObject().put("concurrent_reads", 64);
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(initialYaml, null);
+        provider.storeOverlay(instance, null, new 
ConfigurationOverlaySnapshot(Instant.now(), initial));
+
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String actualHash = manager.getEffectiveConfiguration(instance).hash();
+        String staleHash = 
"sha256:0000000000000000000000000000000000000000000000000000000000000000";
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
staleHash, ops))
+                .isInstanceOf(ConfigurationConflictException.class)
+                .satisfies(e -> {
+                    ConfigurationConflictException conflict = 
(ConfigurationConflictException) e;
+                    assertThat(conflict.expectedHash()).isEqualTo(staleHash);
+                    assertThat(conflict.actualHash()).isEqualTo(actualHash);
+                });
+
+        // Overlay unchanged
+        
assertThat(provider.getOverlay(instance).configuration().cassandraYaml().getInteger("concurrent_reads"))
+                .isEqualTo(64);
+    }
+
+    @Test
+    void testPatchStoreOverlayReturnsFalse()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        JsonObject initialYaml = new JsonObject().put("concurrent_reads", 64);
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(initialYaml, null);
+        ConfigurationOverlaySnapshot initialSnapshot = new 
ConfigurationOverlaySnapshot(Instant.now(), initial);
+
+        ConfigurationProvider rejectingProvider = new ConfigurationProvider()
+        {
+            private ConfigurationOverlaySnapshot stored = initialSnapshot;
+
+            @Override
+            public ConfigurationOverlaySnapshot getOverlay(InstanceMetadata 
inst)
+            {
+                return stored;
+            }
+
+            @Override
+            public boolean storeOverlay(InstanceMetadata inst, String 
originalHash,
+                                        ConfigurationOverlaySnapshot 
newSnapshot)
+            {
+                return false;
+            }
+        };
+
+        ConfigurationManager manager = new 
ConfigurationManager(rejectingProvider, BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
effectiveHash, ops))
+                .isInstanceOf(ConfigurationManagerException.class)
+                .isNotInstanceOf(ConfigurationConflictException.class)
+                .hasMessageContaining("Provider rejected the overlay store 
unexpectedly");
+    }
+
+    @Test
+    void testPatchStoreOverlayReturnsFalseWithConflict()
+    {
+        InstanceMetadata instance = mockInstance(1);
+
+        JsonObject initialYaml = new JsonObject().put("concurrent_reads", 64);
+        CassandraConfigurationOverlay initial = new 
CassandraConfigurationOverlay(initialYaml, null);
+        ConfigurationOverlaySnapshot initialSnapshot = new 
ConfigurationOverlaySnapshot(Instant.now(), initial);
+
+        // Overlay changes between storeOverlay rejection and re-read
+        JsonObject changedYaml = new JsonObject().put("concurrent_reads", 256);
+        CassandraConfigurationOverlay changed = new 
CassandraConfigurationOverlay(changedYaml, null);
+        ConfigurationOverlaySnapshot changedSnapshot = new 
ConfigurationOverlaySnapshot(Instant.now(), changed);
+
+        AtomicInteger getOverlayCallCount = new AtomicInteger(0);
+        ConfigurationProvider conflictingProvider = new ConfigurationProvider()
+        {
+            @Override
+            public ConfigurationOverlaySnapshot getOverlay(InstanceMetadata 
inst)
+            {
+                // First call returns initial, second call (after store 
rejection) returns changed
+                return getOverlayCallCount.incrementAndGet() <= 1 ? 
initialSnapshot : changedSnapshot;
+            }
+
+            @Override
+            public boolean storeOverlay(InstanceMetadata inst, String 
originalHash,
+                                        ConfigurationOverlaySnapshot 
newSnapshot)
+            {
+                return false;
+            }
+        };
+
+        ConfigurationManager manager = new 
ConfigurationManager(conflictingProvider, BASE_TEMPLATE);
+        String effectiveHash = 
manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
effectiveHash, ops))
+                .isInstanceOf(ConfigurationConflictException.class)
+                .satisfies(e -> {
+                    ConfigurationConflictException conflict = 
(ConfigurationConflictException) e;
+                    
assertThat(conflict.expectedHash()).isEqualTo(effectiveHash);
+                    
assertThat(conflict.actualHash()).isNotEqualTo(effectiveHash);
+                });
+    }
+
+    @Test
+    void testPatchProviderFailure()
+    {
+        ConfigurationProvider failingProvider = new ConfigurationProvider()
+        {
+            @Override
+            public ConfigurationOverlaySnapshot getOverlay(InstanceMetadata 
instance)
+            {
+                throw new UncheckedIOException(new IOException("provider 
unavailable"));
+            }
+
+            @Override
+            public boolean storeOverlay(InstanceMetadata instance, String 
originalHash,
+                                        @NotNull ConfigurationOverlaySnapshot 
newSnapshot)
+            {
+                throw new UnsupportedOperationException();
+            }
+        };
+
+        ConfigurationManager manager = new 
ConfigurationManager(failingProvider, BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
"sha256:abc", ops))
+                .isInstanceOf(ConfigurationManagerException.class)
+                .isNotInstanceOf(ConfigurationConflictException.class)
+                .hasMessageContaining("Failed to patch configuration")
+                .hasCauseInstanceOf(UncheckedIOException.class);
+    }
+
+    @Test
+    void testPatchConcurrentSameInstance() throws Exception
+    {
+        InstanceMetadata instance = mockInstance(1);
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        int threadCount = 10;
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+        CountDownLatch startLatch = new CountDownLatch(1);
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger conflictCount = new AtomicInteger(0);
+
+        List<Future<?>> futures = new ArrayList<>();
+        for (int i = 0; i < threadCount; i++)
+        {
+            int value = i;
+            futures.add(executor.submit(() -> {
+                try
+                {
+                    startLatch.await();
+                    List<ConfigurationPatchOperation> ops = List.of(
+                            new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                                           
"/configuration/cassandraYaml/concurrent_reads", value));
+                    manager.patchConfiguration(instance, baseHash, ops);
+                    successCount.incrementAndGet();
+                }
+                catch (ConfigurationConflictException e)
+                {
+                    conflictCount.incrementAndGet();
+                }
+                catch (InterruptedException e)
+                {
+                    Thread.currentThread().interrupt();
+                }
+            }));
+        }
+
+        startLatch.countDown();
+        for (Future<?> future : futures)
+        {
+            future.get();
+        }
+        executor.shutdown();
+
+        assertThat(successCount.get()).isEqualTo(1);
+        assertThat(conflictCount.get()).isEqualTo(threadCount - 1);
+    }
+
+    @Test
+    void testPatchDuplicatePathsRejected()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 64),
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
baseHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Duplicate path");
+    }
+
+    @Test
+    void testPatchInvalidPathFormat()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        List<ConfigurationPatchOperation> ops = List.of(
+                new 
ConfigurationPatchOperation(ConfigurationPatchOperation.Op.ADD,
+                                               "/invalid/path", 42));
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
baseHash, ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Path must start with");
+    }
+
+    @Test
+    void testPatchEmptyOperationsRejected()
+    {
+        ConfigurationManager manager = new ConfigurationManager(provider, 
BASE_TEMPLATE);
+        InstanceMetadata instance = mockInstance(1);
+        String baseHash = manager.getEffectiveConfiguration(instance).hash();
+
+        assertThatThrownBy(() -> manager.patchConfiguration(instance, 
baseHash, List.of()))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("must not be empty");
+    }
+
     private static InstanceMetadata mockInstance(int id)
     {
         InstanceMetadata instance = mock(InstanceMetadata.class);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplierTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplierTest.java
new file mode 100644
index 00000000..fe6f25de
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchApplierTest.java
@@ -0,0 +1,667 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.ADD;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.REMOVE;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.REPLACE;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link ConfigurationPatchApplier}
+ */
+class ConfigurationPatchApplierTest
+{
+    private static final CassandraConfigurationOverlay EMPTY_OVERLAY = new 
CassandraConfigurationOverlay(null, null);
+
+    private CassandraConfigurationOverlay baseConfig;
+
+    @BeforeEach
+    void setUp()
+    {
+        JsonObject effectiveYaml = new JsonObject()
+                .put("cluster_name", "TestCluster")
+                .put("concurrent_reads", 32)
+                .put("memtable", new JsonObject()
+                        .put("configurations", new JsonObject()
+                                .put("trie", new JsonObject()
+                                        .put("class_name", "TrieMemtable")
+                                        .put("max_shard_count", 4))
+                                .put("skiplist", new JsonObject()
+                                        .put("class_name", 
"SkipListMemtable"))));
+
+        Map<String, String> effectiveOpts = new LinkedHashMap<>();
+        effectiveOpts.put("-Xmx", "4g");
+        effectiveOpts.put("-Dcassandra.ring_delay_ms", "60000");
+
+        baseConfig = new CassandraConfigurationOverlay(effectiveYaml, 
effectiveOpts);
+    }
+
+    // --- Top-level cassandraYaml operations ---
+
+    @Test
+    void testAddTopLevelKey()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/new_key", "new_value"));
+
+        
assertThat(newOverlay.cassandraYaml().getString("new_key")).isEqualTo("new_value");
+    }
+
+    @Test
+    void testAddOverwritesExistingOverlayValue()
+    {
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(
+                new JsonObject().put("concurrent_reads", 64), null);
+
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
overlay,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/concurrent_reads", 256));
+
+        
assertThat(newOverlay.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(256);
+    }
+
+    @Test
+    void testRemoveTopLevelKey()
+    {
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(
+                new JsonObject().put("concurrent_reads", 64), null);
+
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
overlay,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/cassandraYaml/concurrent_reads", null));
+
+        
assertThat(newOverlay.cassandraYaml().containsKey("concurrent_reads")).isFalse();
+    }
+
+    @Test
+    void testRemoveTemplateOnlyKeyFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/cassandraYaml/cluster_name", null)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in overlay");
+    }
+
+    @Test
+    void testReplaceExistingKey()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/concurrent_reads", 128));
+
+        
assertThat(newOverlay.cassandraYaml().getInteger("concurrent_reads")).isEqualTo(128);
+    }
+
+    @Test
+    void testReplaceAbsentKeyFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/nonexistent", 42)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in effective config");
+    }
+
+    @Test
+    void testTestMatchingValue()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 32));
+
+        assertThat(newOverlay.cassandraYaml()).isEmpty();
+    }
+
+    @Test
+    void testTestMismatchFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 999)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed: expected 999 but found 
32");
+    }
+
+    @Test
+    void testTestAbsentPathFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/nonexistent", "x")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("path does not exist in effective 
config");
+    }
+
+    // --- Nested cassandraYaml operations (copy-siblings) ---
+
+    @Test
+    void testAddNestedKeyCopiesSiblings()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/compression", 
"lz4"));
+
+        JsonObject memtable = 
newOverlay.cassandraYaml().getJsonObject("memtable");
+        JsonObject trie = 
memtable.getJsonObject("configurations").getJsonObject("trie");
+        // New key added
+        assertThat(trie.getString("compression")).isEqualTo("lz4");
+        // Siblings copied from effective config
+        assertThat(trie.getString("class_name")).isEqualTo("TrieMemtable");
+        assertThat(trie.getInteger("max_shard_count")).isEqualTo(4);
+
+        JsonObject skiplist = 
memtable.getJsonObject("configurations").getJsonObject("skiplist");
+        
assertThat(skiplist.getString("class_name")).isEqualTo("SkipListMemtable");
+    }
+
+    @Test
+    void testReplaceNestedKeyCopiesSiblings()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"ShardedMemtable"));
+
+        JsonObject trie = newOverlay.cassandraYaml().getJsonObject("memtable")
+                                
.getJsonObject("configurations").getJsonObject("trie");
+        assertThat(trie.getString("class_name")).isEqualTo("ShardedMemtable");
+        assertThat(trie.getInteger("max_shard_count")).isEqualTo(4);
+    }
+
+    @Test
+    void testNestedEditPinsSiblingsAgainstBaseDrift()
+    {
+        // Editing one nested leaf copies the whole top-level block's leaves 
into the overlay, pinning
+        // the siblings that existed at edit time against later base-template 
drift. Keys added to the
+        // base block afterwards are not pinned - the deep merge still 
surfaces them.
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"ShardedMemtable"));
+
+        // Base template drifts after the overlay was written: an existing 
sibling changes value and a
+        // brand-new sibling key appears.
+        JsonObject driftedBaseYaml = baseConfig.cassandraYaml().copy();
+        JsonObject driftedTrie = driftedBaseYaml.getJsonObject("memtable")
+                                                
.getJsonObject("configurations").getJsonObject("trie");
+        driftedTrie.put("max_shard_count", 99);       // existing sibling 
drifts
+        driftedTrie.put("new_base_field", "fromBase"); // new key added to base
+
+        JsonObject effective = 
ConfigUtils.mergeConfigurations(driftedBaseYaml, newOverlay.cassandraYaml());
+        JsonObject trie = 
effective.getJsonObject("memtable").getJsonObject("configurations").getJsonObject("trie");
+
+        assertThat(trie.getString("class_name")).isEqualTo("ShardedMemtable"); 
// the edit
+        assertThat(trie.getInteger("max_shard_count")).isEqualTo(4);           
// pinned, not the drifted 99
+        assertThat(trie.getString("new_base_field")).isEqualTo("fromBase");    
// new base key still surfaces
+    }
+
+    @Test
+    void testRemoveOverlaidLeafRevertsToCurrentBaseValue()
+    {
+        // trie/max_shard_count is overlaid at 8; removing it from the overlay 
lets the base value surface
+        // again - and specifically the base value at merge time, not the 
value the overlay held.
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(new JsonObject()
+                .put("memtable", new JsonObject()
+                        .put("configurations", new JsonObject()
+                                .put("trie", new 
JsonObject().put("max_shard_count", 8)))), null);
+
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
overlay,
+                new ConfigurationPatchOperation(REMOVE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/max_shard_count", 
null));
+
+        // Base drifts to 16 after removal; the effective value tracks the 
current base, not the removed 8.
+        JsonObject driftedBaseYaml = baseConfig.cassandraYaml().copy();
+        
driftedBaseYaml.getJsonObject("memtable").getJsonObject("configurations")
+                       .getJsonObject("trie").put("max_shard_count", 16);
+
+        JsonObject effective = 
ConfigUtils.mergeConfigurations(driftedBaseYaml, newOverlay.cassandraYaml());
+        JsonObject trie = 
effective.getJsonObject("memtable").getJsonObject("configurations").getJsonObject("trie");
+
+        assertThat(trie.getInteger("max_shard_count")).isEqualTo(16); // 
reverted to current base value
+    }
+
+    @Test
+    void testMultipleNestedOpsOnSameTopLevelKey()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"ShardedMemtable"),
+                new ConfigurationPatchOperation(REPLACE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/max_shard_count", 
8));
+
+        JsonObject trie = newOverlay.cassandraYaml().getJsonObject("memtable")
+                                
.getJsonObject("configurations").getJsonObject("trie");
+        assertThat(trie.getString("class_name")).isEqualTo("ShardedMemtable");
+        assertThat(trie.getInteger("max_shard_count")).isEqualTo(8);
+
+        JsonObject skiplist = 
newOverlay.cassandraYaml().getJsonObject("memtable")
+                                    
.getJsonObject("configurations").getJsonObject("skiplist");
+        
assertThat(skiplist.getString("class_name")).isEqualTo("SkipListMemtable");
+    }
+
+    @Test
+    void testRemoveNestedKeyFromOverlay()
+    {
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(new JsonObject()
+                .put("memtable", new JsonObject()
+                        .put("configurations", new JsonObject()
+                                .put("trie", new JsonObject()
+                                        .put("class_name", "ShardedMemtable")
+                                        .put("max_shard_count", 8))
+                                .put("skiplist", new JsonObject()
+                                        .put("class_name", 
"SkipListMemtable")))), null);
+
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
overlay,
+                new ConfigurationPatchOperation(REMOVE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/max_shard_count", 
null));
+
+        JsonObject trie = newOverlay.cassandraYaml().getJsonObject("memtable")
+                                
.getJsonObject("configurations").getJsonObject("trie");
+        assertThat(trie.containsKey("max_shard_count")).isFalse();
+        assertThat(trie.getString("class_name")).isEqualTo("ShardedMemtable");
+    }
+
+    @Test
+    void testRemoveNestedKeyNotInOverlayFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REMOVE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", null)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in overlay");
+    }
+
+    @Test
+    void testAddNestedKeyParentAbsentFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD,
+                        
"/configuration/cassandraYaml/nonexistent_parent/child/leaf", "value")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("parent path does not exist");
+    }
+
+    @Test
+    void testTestNestedValue()
+    {
+        applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"TrieMemtable"));
+    }
+
+    @Test
+    void testTestObjectValue()
+    {
+        // TEST against an object-valued path. The request handler reads the 
value via JsonObject.getValue,
+        // so the expected value is a JsonObject - the same type resolveValue 
returns.
+        applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie",
+                        new JsonObject().put("class_name", 
"TrieMemtable").put("max_shard_count", 4)));
+    }
+
+    @Test
+    void testTestObjectValueMismatchFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie",
+                        new JsonObject().put("class_name", 
"TrieMemtable").put("max_shard_count", 99))))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    @Test
+    void testTestArrayValue()
+    {
+        // TEST against an array-valued path. The expected value is a 
JsonArray, matching what
+        // resolveValue returns for array values.
+        JsonObject effectiveYaml = baseConfig.cassandraYaml().copy()
+                .put("data_file_directories", new 
JsonArray().add("/data1").add("/data2"));
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                effectiveYaml, baseConfig.extraJvmOpts());
+
+        applyOps(effective, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        "/configuration/cassandraYaml/data_file_directories",
+                        new JsonArray().add("/data1").add("/data2")));
+    }
+
+    @Test
+    void testTestArrayValueMismatchFails()
+    {
+        JsonObject effectiveYaml = baseConfig.cassandraYaml().copy()
+                .put("data_file_directories", new 
JsonArray().add("/data1").add("/data2"));
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                effectiveYaml, baseConfig.extraJvmOpts());
+
+        assertThatThrownBy(() -> applyOps(effective, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        "/configuration/cassandraYaml/data_file_directories",
+                        new JsonArray().add("/data1").add("/other"))))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    @Test
+    void testTestNestedValueMismatchFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"WrongValue")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    // --- Array-valued top-level keys ---
+
+    @Test
+    void testAddTopLevelArrayValue()
+    {
+        List<String> directories = List.of("/data1", "/data2");
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/data_file_directories", directories));
+
+        
assertThat(newOverlay.cassandraYaml().getJsonArray("data_file_directories"))
+                .containsExactly("/data1", "/data2");
+    }
+
+    @Test
+    void testReplaceTopLevelArrayValue()
+    {
+        JsonObject effectiveYaml = baseConfig.cassandraYaml().copy()
+                .put("data_file_directories", List.of("/old_data"));
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                effectiveYaml, baseConfig.extraJvmOpts());
+
+        CassandraConfigurationOverlay newOverlay = applyOps(effective, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/data_file_directories",
+                        List.of("/new_data1", "/new_data2")));
+
+        
assertThat(newOverlay.cassandraYaml().getJsonArray("data_file_directories"))
+                .containsExactly("/new_data1", "/new_data2");
+    }
+
+    @Test
+    void testNestedPathIntoArrayValueFails()
+    {
+        JsonObject effectiveYaml = baseConfig.cassandraYaml().copy()
+                .put("seed_provider", List.of(Map.of("class_name", 
"SimpleSeedProvider")));
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                effectiveYaml, baseConfig.extraJvmOpts());
+
+        assertThatThrownBy(() -> applyOps(effective, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE,
+                        
"/configuration/cassandraYaml/seed_provider/0/class_name", "OtherProvider")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("intermediate value is not an object");
+    }
+
+    // --- extraJvmOpts operations ---
+
+    @Test
+    void testAddJvmOpt()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xms", "2g"));
+
+        assertThat(newOverlay.extraJvmOpts()).containsEntry("-Xms", "2g");
+    }
+
+    @Test
+    void testRemoveJvmOptFromOverlay()
+    {
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null,
+                new LinkedHashMap<>(Map.of("-Xmx", "8g")));
+
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
overlay,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/extraJvmOpts/-Xmx", null));
+
+        assertThat(newOverlay.extraJvmOpts()).doesNotContainKey("-Xmx");
+    }
+
+    @Test
+    void testReplaceJvmOptAbsentFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/extraJvmOpts/-Xms", "2g")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in effective 
extraJvmOpts");
+    }
+
+    @Test
+    void testTestJvmOptValue()
+    {
+        applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST, 
"/configuration/extraJvmOpts/-Xmx", "4g"));
+    }
+
+    @Test
+    void testTestJvmOptMismatchFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(TEST, 
"/configuration/extraJvmOpts/-Xmx", "16g")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    @Test
+    void testAddInvalidJvmOptKeyFails()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/invalidKey", "value")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Invalid JVM option key 'invalidKey'");
+    }
+
+    @Test
+    void testAddConflictingBooleanJvmOptFails()
+    {
+        Map<String, String> effectiveOpts = new LinkedHashMap<>();
+        effectiveOpts.put("-Xmx", "4g");
+        effectiveOpts.put("-XX:+UseG1GC", "");
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                baseConfig.cassandraYaml(), effectiveOpts);
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null,
+                new LinkedHashMap<>(Map.of("-XX:+UseG1GC", "")));
+
+        assertThatThrownBy(() -> applyOps(effective, overlay,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:-UseG1GC", "")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Conflicting boolean JVM option");
+    }
+
+    @Test
+    void testAddConflictingBooleanJvmOptAgainstBaseFails()
+    {
+        // Conflicting option exists only in the effective config (i.e. base 
template), not the overlay.
+        // The overlay-only check would miss this; the effective-config check 
must catch it.
+        Map<String, String> effectiveOpts = new LinkedHashMap<>();
+        effectiveOpts.put("-XX:+UseG1GC", "");
+        CassandraConfigurationOverlay effective = new 
CassandraConfigurationOverlay(
+                baseConfig.cassandraYaml(), effectiveOpts);
+
+        assertThatThrownBy(() -> applyOps(effective, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:-UseG1GC", "")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Conflicting boolean JVM option");
+    }
+
+    @Test
+    void testReplaceBooleanJvmOptByRemovingThenAdding()
+    {
+        // -XX:+UseG1GC lives only in the overlay (not the base template), so 
removing it from the
+        // overlay clears it from the effective config, and the subsequent add 
of -XX:-UseG1GC applies
+        // against the now-conflict-free effective configuration.
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null,
+                new LinkedHashMap<>(Map.of("-XX:+UseG1GC", "")));
+
+        Map<String, String> baseOpts = new LinkedHashMap<>();
+        baseOpts.put("-Xmx", "4g");
+        CassandraConfigurationOverlay base = new CassandraConfigurationOverlay(
+                baseConfig.cassandraYaml(), baseOpts);
+
+        CassandraConfigurationOverlay newOverlay = applyOps(base, overlay,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/extraJvmOpts/-XX:+UseG1GC", null),
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:-UseG1GC", ""));
+
+        
assertThat(newOverlay.extraJvmOpts()).doesNotContainKey("-XX:+UseG1GC");
+        assertThat(newOverlay.extraJvmOpts()).containsEntry("-XX:-UseG1GC", 
"");
+    }
+
+    // --- Atomicity ---
+
+    @Test
+    void testTestFailurePreventsAllMutations()
+    {
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/new_key", "value"),
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 999)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    @Test
+    void testMultipleOpsAppliedAtomically()
+    {
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/concurrent_writes", 64),
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xms", "2g"),
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 32));
+
+        
assertThat(newOverlay.cassandraYaml().getInteger("concurrent_writes")).isEqualTo(64);
+        assertThat(newOverlay.extraJvmOpts()).containsEntry("-Xms", "2g");
+    }
+
+    // --- Sequential (RFC 6902 section 5) operation semantics ---
+
+    @Test
+    void testReplaceThenTestSeesUpdatedValue()
+    {
+        // "Change X, then assert X now holds the new value" - the test op 
must observe the prior replace.
+        applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/concurrent_reads", 64),
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 64));
+    }
+
+    @Test
+    void testTestAgainstStaleValueFails()
+    {
+        // After the replace, the value is 64, so a test for the original 32 
must fail.
+        assertThatThrownBy(() -> applyOps(baseConfig, EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/concurrent_reads", 64),
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/concurrent_reads", 32)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Test failed");
+    }
+
+    @Test
+    void testAddObjectThenAddChild()
+    {
+        // Build-up: create a new nested object, then add a child into it in 
the same patch.
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD,
+                        
"/configuration/cassandraYaml/memtable/configurations/custom", new 
JsonObject()),
+                new ConfigurationPatchOperation(ADD,
+                        
"/configuration/cassandraYaml/memtable/configurations/custom/class_name", 
"CustomMemtable"));
+
+        JsonObject custom = 
newOverlay.cassandraYaml().getJsonObject("memtable")
+                                    
.getJsonObject("configurations").getJsonObject("custom");
+        assertThat(custom.getString("class_name")).isEqualTo("CustomMemtable");
+    }
+
+    @Test
+    void testAddTopLevelThenReplaceChild()
+    {
+        // Create a new top-level section, then replace a field within it.
+        CassandraConfigurationOverlay newOverlay = applyOps(baseConfig, 
EMPTY_OVERLAY,
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/new_section",
+                        new JsonObject().put("a", 1)),
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/new_section/a", 2));
+
+        
assertThat(newOverlay.cassandraYaml().getJsonObject("new_section").getInteger("a")).isEqualTo(2);
+    }
+
+    @Test
+    void testRemoveThenRemoveChildFails()
+    {
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(new JsonObject()
+                .put("memtable", new JsonObject()
+                        .put("configurations", new JsonObject()
+                                .put("trie", new JsonObject()
+                                        .put("class_name", "ShardedMemtable")
+                                        .put("max_shard_count", 8)))), null);
+
+        // Removing 'trie' first makes the subsequent remove of 
trie/class_name reference a path that no
+        // longer exists in the overlay - it must fail rather than silently 
no-op.
+        assertThatThrownBy(() -> applyOps(baseConfig, overlay,
+                new ConfigurationPatchOperation(REMOVE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie", null),
+                new ConfigurationPatchOperation(REMOVE,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", null)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in overlay");
+    }
+
+    @Test
+    void testRemoveOverlayOnlyTopLevelThenWriteChildFails()
+    {
+        // 'custom_section' exists only in the overlay. Removing it clears it 
from the effective config,
+        // so replacing a child of it afterwards must fail instead of 
resurrecting it.
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(new JsonObject()
+                .put("custom_section", new JsonObject().put("a", 1)), null);
+
+        assertThatThrownBy(() -> applyOps(baseConfig, overlay,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/cassandraYaml/custom_section", null),
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/custom_section/a", 2)))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("does not exist in effective config");
+    }
+
+    @Test
+    void testRemoveOverlayBooleanOptThenAddOppositeStillConflictsAgainstBase()
+    {
+        // +UseG1GC exists in BOTH the base and the overlay. Removing the 
overlay copy leaves the base's
+        // +UseG1GC in the effective config, so adding -UseG1GC must still be 
rejected as a conflict.
+        Map<String, String> baseOpts = new LinkedHashMap<>();
+        baseOpts.put("-XX:+UseG1GC", "");
+        CassandraConfigurationOverlay base = new CassandraConfigurationOverlay(
+                baseConfig.cassandraYaml(), baseOpts);
+        CassandraConfigurationOverlay overlay = new 
CassandraConfigurationOverlay(null,
+                new LinkedHashMap<>(Map.of("-XX:+UseG1GC", "")));
+
+        assertThatThrownBy(() -> applyOps(base, overlay,
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/extraJvmOpts/-XX:+UseG1GC", null),
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:-UseG1GC", "")))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Conflicting boolean JVM option");
+    }
+
+    private static CassandraConfigurationOverlay 
applyOps(CassandraConfigurationOverlay base,
+                                                          
CassandraConfigurationOverlay overlay,
+                                                          
ConfigurationPatchOperation... ops)
+    {
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed =
+                ConfigurationPatchValidator.validate(List.of(ops));
+        return ConfigurationPatchApplier.apply(parsed, base, overlay);
+    }
+}
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidatorTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidatorTest.java
new file mode 100644
index 00000000..391b51e8
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/configmanagement/ConfigurationPatchValidatorTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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.cassandra.sidecar.configmanagement;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.ADD;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.REMOVE;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.REPLACE;
+import static 
org.apache.cassandra.sidecar.configmanagement.ConfigurationPatchOperation.Op.TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link ConfigurationPatchValidator}
+ */
+class ConfigurationPatchValidatorTest
+{
+    @Test
+    void testRejectsEmptyOperationsList()
+    {
+        assertThatThrownBy(() -> 
ConfigurationPatchValidator.validate(List.of()))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("must not be empty");
+    }
+
+    @Test
+    void testRejectsRemoveWithValue()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/cassandraYaml/key", "value"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("REMOVE' must not have a value");
+    }
+
+    @Test
+    void testRejectsAddWithoutValue()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/key", null));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("requires a value");
+    }
+
+    @Test
+    void testRejectsReplaceWithoutValue()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/key", null));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("requires a value");
+    }
+
+    @Test
+    void testRejectsTestWithoutValue()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/key", null));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("requires a value");
+    }
+
+    @Test
+    void testRejectsInvalidPathPrefix()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, "/invalid/path", 42));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Path must start with");
+    }
+
+    @Test
+    void testRejectsPathWithNoKeyAfterSection()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/", 42));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Path must specify at least one key 
after section");
+    }
+
+    @Test
+    void testRejectsNestedExtraJvmOpts()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo/nested", "bar"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("extraJvmOpts paths must be flat");
+    }
+
+    @Test
+    void testRejectsEmptySegmentInPath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml//key", "value"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("empty segment");
+    }
+
+    @Test
+    void testRejectsDuplicateMutationPaths()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/key", "a"),
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/key", "b"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Duplicate path");
+    }
+
+    @Test
+    void testAllowsTestAndMutationOnSamePath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(TEST, 
"/configuration/cassandraYaml/key", "old"),
+                new ConfigurationPatchOperation(REPLACE, 
"/configuration/cassandraYaml/key", "new"));
+
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed = 
ConfigurationPatchValidator.validate(ops);
+
+        assertThat(parsed).hasSize(2);
+    }
+
+    @Test
+    void testParsesTopLevelCassandraYamlPath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/cassandraYaml/concurrent_reads", 64));
+
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed = 
ConfigurationPatchValidator.validate(ops);
+
+        assertThat(parsed).hasSize(1);
+        assertThat(parsed.get(0).section()).isEqualTo("cassandraYaml");
+        assertThat(parsed.get(0).topLevelKey()).isEqualTo("concurrent_reads");
+        assertThat(parsed.get(0).nestedSegments()).isEmpty();
+        assertThat(parsed.get(0).isNested()).isFalse();
+    }
+
+    @Test
+    void testParsesNestedCassandraYamlPath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD,
+                        
"/configuration/cassandraYaml/memtable/configurations/trie/class_name", 
"value"));
+
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed = 
ConfigurationPatchValidator.validate(ops);
+
+        assertThat(parsed.get(0).section()).isEqualTo("cassandraYaml");
+        assertThat(parsed.get(0).topLevelKey()).isEqualTo("memtable");
+        
assertThat(parsed.get(0).nestedSegments()).containsExactly("configurations", 
"trie", "class_name");
+        assertThat(parsed.get(0).isNested()).isTrue();
+    }
+
+    @Test
+    void testParsesExtraJvmOptsPath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xmx", "8g"));
+
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed = 
ConfigurationPatchValidator.validate(ops);
+
+        assertThat(parsed.get(0).section()).isEqualTo("extraJvmOpts");
+        assertThat(parsed.get(0).topLevelKey()).isEqualTo("-Xmx");
+        assertThat(parsed.get(0).nestedSegments()).isEmpty();
+    }
+
+    // --- extraJvmOpts key allowlist tests ---
+
+    @Test
+    void testAcceptsValidSystemPropertyKey()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dcassandra.jmx.local.port", "7199"));
+
+        List<ConfigurationPatchValidator.ParsedPatchOperation> parsed = 
ConfigurationPatchValidator.validate(ops);
+
+        assertThat(parsed).hasSize(1);
+        
assertThat(parsed.get(0).topLevelKey()).isEqualTo("-Dcassandra.jmx.local.port");
+    }
+
+    @Test
+    void testAcceptsValidXFlag()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xms", "2g"));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testAcceptsValidXXBooleanFlag()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:+UseG1GC", ""));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testAcceptsValidXXValueFlag()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:MaxGCPauseMillis", "200"));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testRejectsJavaAgentKey()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-javaagent", "/tmp/malicious.jar"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Invalid JVM option key '-javaagent'");
+    }
+
+    @Test
+    void testRejectsAgentPathKey()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-agentpath", "/tmp/agent.so"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Invalid JVM option key '-agentpath'");
+    }
+
+    @Test
+    void testRejectsBlockedOnOutOfMemoryError()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:OnOutOfMemoryError", "kill -9 %p"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessage("Blocked extraJvmOpts key 
'-XX:OnOutOfMemoryError': this JVM option is not allowed");
+    }
+
+    @Test
+    void testRejectsBlockedOnError()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-XX:OnError", "rm -rf /"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessage("Blocked extraJvmOpts key '-XX:OnError': this JVM 
option is not allowed");
+    }
+
+    @Test
+    void testRejectsPathBearingJvmOpts()
+    {
+        // Options that write to arbitrary filesystem paths must be blocked by 
key, since the value
+        // pattern legitimately permits absolute paths for Cassandra system 
properties.
+        List<String> blockedPathOpts = List.of(
+                "-XX:ErrorFile", "-XX:HeapDumpPath", "-XX:LogFile",
+                "-XX:FlightRecorderOptions", "-XX:StartFlightRecording",
+                "-Xloggc", "-Xlog", "-Xbootclasspath");
+
+        for (String key : blockedPathOpts)
+        {
+            List<ConfigurationPatchOperation> ops = List.of(
+                    new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/" + key, "/tmp/evil"));
+
+            assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                    .isInstanceOf(ConfigurationPatchException.class)
+                    .hasMessage("Blocked extraJvmOpts key '" + key + "': this 
JVM option is not allowed");
+        }
+    }
+
+    @Test
+    void testRejectsKeyWithEqualsSign()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo=bar", "value"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Invalid JVM option key '-Dfoo=bar'");
+    }
+
+    @Test
+    void testRejectsKeyWithShellMetacharacters()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo;echo pwned", "value"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("Invalid JVM option key '-Dfoo;echo 
pwned'");
+    }
+
+    // --- extraJvmOpts value validation tests ---
+
+    @Test
+    void testAcceptsValidJvmOptValue()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xmx", "4g"));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testAcceptsJvmOptValueWithAbsolutePath()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dcassandra.logdir", "/var/log/cassandra"));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testAcceptsJvmOptValueWithJson()
+    {
+        String jsonValue = "{\"class_name\":\"SizeTieredCompactionStrategy\","
+                           + 
"\"parameters\":{\"min_threshold\":\"4\",\"max_threshold\":\"32\"}}";
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dcassandra.settings.default_compaction",
+                                                jsonValue));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testAcceptsJvmOptValueWithCommas()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dcassandra.seed_provider",
+                                                
"org.apache.cassandra.locator.SimpleSeedProvider:127.0.0.1,127.0.0.2"));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+    @Test
+    void testRejectsJvmOptValueWithWhitespace()
+    {
+        // Whitespace is rejected because bin/cassandra word-splits unquoted 
values during `eval`,
+        // silently truncating a value like "hello world" to "hello".
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo", "hello world"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("disallowed characters");
+    }
+
+    @Test
+    void testRejectsJvmOptValueWithPathTraversal()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dcassandra.logdir", "/tmp/../../etc/cron.d"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("path traversal");
+    }
+
+    @Test
+    void testRejectsJvmOptValueWithShellMetacharacters()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo", "val;rm -rf /"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("disallowed characters");
+    }
+
+    @Test
+    void testRejectsJvmOptValueWithBacktick()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Dfoo", "`whoami`"));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("disallowed characters");
+    }
+
+    @Test
+    void testRejectsJvmOptValueExceedingMaxLength()
+    {
+        String longValue = "a".repeat(513);
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(ADD, 
"/configuration/extraJvmOpts/-Xmx", longValue));
+
+        assertThatThrownBy(() -> ConfigurationPatchValidator.validate(ops))
+                .isInstanceOf(ConfigurationPatchException.class)
+                .hasMessageContaining("disallowed characters or exceeds 512");
+    }
+
+    @Test
+    void testAllowsRemoveWithoutValueValidation()
+    {
+        List<ConfigurationPatchOperation> ops = List.of(
+                new ConfigurationPatchOperation(REMOVE, 
"/configuration/extraJvmOpts/-Xmx", null));
+
+        assertThat(ConfigurationPatchValidator.validate(ops)).hasSize(1);
+    }
+
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to