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

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kit.git

commit a5f68b1dd94b7a317e41380edddd221005940c89
Author: Jinal Sarvaiya <[email protected]>
AuthorDate: Tue Nov 3 09:37:04 2026 +0000

    fix#100: harden ShipStageResult parser against fenced-envelope and 
size-bypass regressions
---
 .../camel/kit/ship/controller/ShipStageResult.java | 14 ++++++--
 .../kit/ship/controller/ShipStageResultTest.java   | 42 ++++++++++++++++++++++
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
index ca0e43e4..06f43917 100644
--- 
a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
+++ 
b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/ship/controller/ShipStageResult.java
@@ -80,9 +80,9 @@ record ShipStageResult(
         WireResult wire;
         boolean legacy = false;
         try {
+            utf8(assistantText, MAX_ASSISTANT_BYTES, "assistant response");
             String normalized = extractJsonObject(assistantText);
-            JsonNode document
-                    = JSON.readValue(utf8(normalized, MAX_ASSISTANT_BYTES, 
"assistant response"), JsonNode.class);
+            JsonNode document = JSON.readValue(normalized, JsonNode.class);
             if (document instanceof ObjectNode object && 
object.path("schemaVersion").isIntegralNumber()
                     && object.path("schemaVersion").intValue() == 1) {
                 legacy = true;
@@ -294,11 +294,19 @@ record ShipStageResult(
         }
         Matcher m = JSON_FENCE.matcher(text);
         if (m.find()) {
-            return m.group(1).strip();
+            String first = m.group(1).strip();
+            if (m.find()) {
+                return stripped;
+            }
+            return first;
         }
         int start = text.indexOf('{');
         int end = text.lastIndexOf('}');
         if (start >= 0 && end > start) {
+            String prefix = text.substring(0, start).strip();
+            if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) == 
'[') {
+                return stripped;
+            }
             return text.substring(start, end + 1);
         }
         return stripped;
diff --git 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
index 3aec0ff1..0260c3a7 100644
--- 
a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
+++ 
b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/ship/controller/ShipStageResultTest.java
@@ -419,6 +419,48 @@ class ShipStageResultTest {
         assertRejected(Stage.PLAN, unsorted, "unsorted routes");
     }
 
+    @Test
+    void acceptsSingleFencedResponse() throws Exception {
+        ObjectNode valid = result("149-local-controller", null);
+        String fenced = "Here is the result:\n```json\n" + 
JSON.writeValueAsString(valid) + "\n```\n";
+        ShipStageResult parsed = ShipStageResult.parse(Stage.DISCOVERY, 
fenced);
+        assertEquals("149-local-controller", parsed.pipelineId());
+    }
+
+    @Test
+    void rejectsConflictingFencedEnvelopes() throws Exception {
+        ObjectNode first = result("149-local-controller", null);
+        ObjectNode second = result("149-local-controller", null);
+        second.put("materialAmbiguity", true);
+        second.withArray("unansweredQuestions").addObject()
+                .put("question", "Which retry 
limit?").putNull("defaultApplied");
+        String multiFence = "First:\n```json\n" + 
JSON.writeValueAsString(first)
+                            + "\n```\nCorrection:\n```json\n" + 
JSON.writeValueAsString(second) + "\n```\n";
+        assertRejected(Stage.DISCOVERY, multiFence, "conflicting fenced 
envelopes");
+    }
+
+    @Test
+    void rejectsArrayWrappedEnvelope() throws Exception {
+        ObjectNode valid = result("149-local-controller", null);
+        String array = "[" + JSON.writeValueAsString(valid) + "]";
+        assertRejected(Stage.DISCOVERY, array, "array-wrapped envelope");
+    }
+
+    @Test
+    void rejectsOversizedProseAroundValidFence() throws Exception {
+        ObjectNode valid = result("149-local-controller", null);
+        String prose = "x".repeat(1024 * 1024 + 1);
+        String oversized = prose + "\n```json\n" + 
JSON.writeValueAsString(valid) + "\n```\n";
+        assertRejected(Stage.DISCOVERY, oversized, "oversized prose around 
valid fence");
+    }
+
+    @Test
+    void rejectsInvalidUnicodeOutsideFence() throws Exception {
+        ObjectNode valid = result("149-local-controller", null);
+        String fenced = "\uD800\n```json\n" + JSON.writeValueAsString(valid) + 
"\n```\n";
+        assertRejected(Stage.DISCOVERY, fenced, "lone surrogate outside 
fence");
+    }
+
     private static void assertPolicyMutationRejected(ObjectNode valid, String 
field, String value)
             throws Exception {
         ObjectNode invalid = valid.deepCopy();

Reply via email to