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();
