This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4627b8694d9b CAMEL-24897: camel-jbang-mcp - camel_write_file: drop the
validate flag, an agent turns it off
4627b8694d9b is described below
commit 4627b8694d9bcc848b3876671104f81dcd31a7bc
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 22 14:54:49 2026 +0200
CAMEL-24897: camel-jbang-mcp - camel_write_file: drop the validate flag, an
agent turns it off
Fixes https://issues.apache.org/jira/browse/CAMEL-24897 (part of
CAMEL-24886)
`camel_write_file` had a boolean `validate` argument, default true, and its
refusal message ended with "(validate=false writes it anyway)". In the
camel-jbang-mcp stepwise benchmark the local model set `validate: false` on its
own: 9 of 26 writes in series s6, 26 of 337 in s3, often on the first write of
a step with no refusal to provoke it. The invalid file then reloaded, the app
ran without routes (a single route file has nothing to restore), and the model
read the validator's repor [...]
The flag existed for a person who knows better; for an agent it is a bypass
of the one deterministic check we have. The argument is gone from the shared
tool definition, the MCP wrapper and the camel-jbang views' registry, the write
always validates, and the refusal message no longer offers the bypass.
`writeFile(ctx, dir, file, content, validate)` keeps its signature for other
callers. Tests and the two doc pages updated, upgrade note added.
---
.../modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 3 +++
.../user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc | 2 +-
.../user-manual/modules/ROOT/pages/camel-jbang-tui.adoc | 2 +-
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 7 +++----
.../dsl/jbang/core/commands/ai/AuthoringToolsTest.java | 8 ++++----
.../dsl/jbang/core/commands/mcp/AuthoringTools.java | 3 +--
.../dsl/jbang/core/commands/mcp/AuthoringToolsTest.java | 4 ++--
.../camel/dsl/jbang/core/commands/tui/McpFacade.java | 17 +++++------------
.../dsl/jbang/core/commands/tui/TuiToolRegistry.java | 4 ++--
.../jbang/core/commands/tui/McpFacadeWriteFileTest.java | 10 ++++------
10 files changed, 26 insertions(+), 34 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 62262c658fd4..be1c20b5d9c4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -885,6 +885,9 @@ eight seconds for the reload of the written file and
answers with its outcome (`
with the cause and the validator's report, properties, or unknown), so an
agent does not go on with a route that
did not load. A write with no selected integration answers as before.
+The `validate` argument of `camel_write_file` is gone: the write always
validates the content and refuses an
+invalid file. A model given the switch turned it off on its own, and the file
then failed to load.
+
The `camel_run` tool, when no files are named, starts the project with `camel
run --source-dir=.` instead of
listing the directory's files: the directory is watched, so a file added
afterwards (a bean file, a Java class
under `src/main/java`) is part of the app and reloaded in dev mode, and a
`restart` starts the same way. Naming
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index fb83a312dd51..e364f24b4cff 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -333,7 +333,7 @@ project `directory` as an argument, the runtime tools take
the integration `name
| `camel_write_file`
| Writes the complete content of a file in the project directory. YAML and
`.properties` content is validated
- first; invalid content is not written and the errors are returned
(`validate=false` writes anyway). Only a
+ first; invalid content is not written and the errors are returned. Only a
plain file name in the directory is accepted. Nobody is asked before the
write: the MCP client (Claude Code,
Cursor and the others ask before a tool that is not read-only runs) is where
the human sits, and the
`read-only` access level of the security layer hides the tool altogether.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 4c7042e2a347..792689b6575b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -1307,7 +1307,7 @@ the Source tab's save: YAML routes against the Camel YAML
DSL schema (a misspell
instead of `loggingLevel` is reported) plus endpoint URIs and simple
expressions, and `.properties` files
against the catalog of `camel.*` and Spring Boot options. `camel_write_file`
runs that validation itself and
refuses to write an invalid file, returning the errors instead, so a model
fixes them rather than the user
-finding them in the log after the reload (`validate=false` writes anyway).
Both tools are part of the core
+finding them in the log after the reload. Both tools are part of the core
tool set, so local models (Ollama) get them as well.
Simple expressions get two more helpers, because they are what a small model
gets wrong most often
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index cad7f2a5433e..91793577caf4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -208,15 +208,14 @@ public final class AuthoringTools {
.param("directory", "string", DIRECTORY_DESC, false)
.param("file", "string", FILE_PATH_DESC + " (subdirectories
are created)", true)
.param("content", "string", "The complete new content", true)
- .param("validate", "boolean", "Validate before writing
(default true)", false)
.param("camelVersion", "string", VERSION_DESC, false)
.readOnly(false)
.core(true)
.executor((ctx, args) -> {
applyVersion(ctx, args);
Path dir = ctx.resolveDirectory(args.get("directory"));
- return writeFile(ctx, dir, required(args, "file"),
required(args, "content"),
- bool(args, "validate", true)).toJson();
+ // always validated: a model given a switch turns it off
(CAMEL-24897)
+ return writeFile(ctx, dir, required(args, "file"),
required(args, "content"), true).toJson();
}));
registry.accept(tool("camel_run",
@@ -380,7 +379,7 @@ public final class AuthoringTools {
result.put("file", file);
result.put("errors", new JsonArray(errors));
result.put("message", "The file was not written: the content
has validation errors. Fix them and"
- + " call camel_write_file again
(validate=false writes it anyway).");
+ + " call camel_write_file again.");
return result;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index 416f099b0f67..8d95081714ce 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -105,9 +105,9 @@ class AuthoringToolsTest {
assertEquals("object", schema.getString("type"));
JsonObject properties = schema.getMap("properties");
JsonObject file = properties.getMap("file");
- JsonObject validate = properties.getMap("validate");
assertEquals("string", file.getString("type"));
- assertEquals("boolean", validate.getString("type"));
+ // no validate switch: the write always validates (CAMEL-24897)
+ assertEquals(null, properties.get("validate"));
assertEquals(List.of("file", "content"),
List.copyOf(schema.getCollection("required")));
JsonObject logProperties =
ToolRegistry.findTool("camel_get_log").inputSchema().getMap("properties");
JsonObject limit = logProperties.getMap("limit");
@@ -162,10 +162,10 @@ class AuthoringToolsTest {
JsonObject created = call("camel_write_file", new ToolContext(),
Map.of("directory", dir.toString(), "file", "notes.txt",
"content", "logLevel"));
assertEquals("created", created.getString("status"), "other file types
are not validated");
- // validation can be switched off, and a properties file is validated
too
+ // a properties file is validated too, and validation cannot be
switched off (CAMEL-24897)
assertEquals("invalid", call("camel_write_file", new ToolContext(),
Map.of("directory", dir.toString(),
"file", "application.properties", "content",
"camel.main.nme=x")).getString("status"));
- assertEquals("created", call("camel_write_file", new ToolContext(),
Map.of("directory", dir.toString(),
+ assertEquals("invalid", call("camel_write_file", new ToolContext(),
Map.of("directory", dir.toString(),
"file", "application.properties", "content",
"camel.main.nme=x", "validate", false))
.getString("status"));
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 954bd4f04877..bb26bf072701 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -148,10 +148,9 @@ public class AuthoringTools {
@ToolArg(description = "File path relative to the directory
(subdirectories are created)",
required = true) String file,
@ToolArg(description = "The complete new content", required =
true) String content,
- @ToolArg(description = "Validate before writing (default true)",
required = false) Boolean validate,
@ToolArg(description = VERSION_DESC, required = false) String
camelVersion) {
return call("camel_write_file", args("directory", directory, "file",
file, "content", content,
- "validate", validate, "camelVersion", camelVersion));
+ "camelVersion", camelVersion));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = false, openWorldHint = true),
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 14326036a96b..3759d6edfbff 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -86,12 +86,12 @@ class AuthoringToolsTest {
@Test
void filesAreReadAndWrittenInTheGivenDirectory(@TempDir Path dir) throws
Exception {
String route = "- route:\n from:\n uri: timer:tick\n
steps:\n - log:\n message: hi\n";
- JsonObject written = tools.camel_write_file(dir.toString(),
"demo.camel.yaml", route, null, null);
+ JsonObject written = tools.camel_write_file(dir.toString(),
"demo.camel.yaml", route, null);
assertThat(written.getString("status")).isEqualTo("created");
assertThat(Files.readString(dir.resolve("demo.camel.yaml"),
StandardCharsets.UTF_8)).isEqualTo(route);
JsonObject invalid = tools.camel_write_file(dir.toString(),
"demo.camel.yaml",
- route.replace("message", "mesage"), null, null);
+ route.replace("message", "mesage"), null);
assertThat(invalid.getString("status")).isEqualTo("invalid");
assertThat(Files.readString(dir.resolve("demo.camel.yaml"),
StandardCharsets.UTF_8)).isEqualTo(route);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index b44bf2d9c51f..9ddbd0acd55d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1082,18 +1082,11 @@ class McpFacade {
/**
* Writes (creates or replaces) a file in the integration's source
directory, after the user confirmed it in the TUI
- * unless {@code confirm} is false. The file is a path relative to that
directory.
+ * unless {@code confirm} is false. The file is a path relative to that
directory. A YAML route or .properties file
+ * is always validated first (the editor's checks) and not written when it
has errors, so a model fixes them instead
+ * of the user finding them in the log after the reload (CAMEL-24897).
*/
JsonObject writeFile(String name, String file, String content, boolean
confirm) {
- return writeFile(name, file, content, confirm, true);
- }
-
- /**
- * As {@link #writeFile(String, String, String, boolean)}; with {@code
validate} a YAML route or .properties file is
- * validated first (the editor's checks) and not written when it has
errors, so a model fixes them instead of the
- * user finding them in the log after the reload.
- */
- JsonObject writeFile(String name, String file, String content, boolean
confirm, boolean validate) {
IntegrationInfo target = findIntegration(name);
if (target == null) {
return writeError(name != null && !name.isEmpty()
@@ -1119,7 +1112,7 @@ class McpFacade {
if (exists && !Files.isRegularFile(filePath)) {
return writeError(file + " is not a regular file");
}
- if (validate && sourceValidator != null &&
SourceValidator.isValidatableFile(file)) {
+ if (sourceValidator != null &&
SourceValidator.isValidatableFile(file)) {
List<String> errors = sourceValidator.apply(file, content);
if (!errors.isEmpty()) {
JsonObject result = new JsonObject();
@@ -1129,7 +1122,7 @@ class McpFacade {
arr.addAll(errors);
result.put("errors", arr);
result.put("message", "The file was not written: the content
has validation errors. Fix them and"
- + " call camel_write_file again
(validate=false writes it anyway).");
+ + " call camel_write_file again.");
return result;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 882c207a2a23..702f86313e45 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -1392,8 +1392,8 @@ class TuiToolRegistry {
String file = args.get("file") instanceof String s ? s : null;
String content = args.get("content") instanceof String s ? s : null;
boolean confirm = !Boolean.FALSE.equals(args.get("confirm"));
- boolean validate = !Boolean.FALSE.equals(args.get("validate"));
- return Jsoner.serialize(facade.writeFile(name, file, content, confirm,
validate));
+ // the write always validates: the tool has no switch, a model given
one turns it off (CAMEL-24897)
+ return Jsoner.serialize(facade.writeFile(name, file, content,
confirm));
}
private String callValidateSource(Map<String, Object> args) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
index 0d59e4edcbf6..e9bec0b2ee83 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
@@ -186,7 +186,7 @@ class McpFacadeWriteFileTest {
}
@Test
- void invalidContentIsNotWrittenUnlessValidationIsOff(@TempDir Path dir)
throws IOException {
+ void invalidContentIsNeverWritten(@TempDir Path dir) throws IOException {
Files.writeString(dir.resolve("demo.camel.yaml"), "- route: {}\n");
ConfirmingBridge bridge = new ConfirmingBridge(true);
McpFacade facade = facade(dir, true, bridge);
@@ -211,17 +211,15 @@ class McpFacadeWriteFileTest {
assertEquals("invalid", facade.writeFile("demo",
"application.properties", "camel.main.nme=x", true)
.getString("status"));
assertEquals("created", facade.writeFile("demo", "notes.txt",
"logLevel", true).getString("status"));
- // and validation can be switched off (the bridge confirms)
- assertEquals("overwritten",
- facade.writeFile("demo", "demo.camel.yaml", "- log:\n
logLevel: WARN\n", true, false)
- .getString("status"));
+ // validation cannot be switched off: there is no overload that skips
it (CAMEL-24897)
+ assertEquals("- route: {}\n",
Files.readString(dir.resolve("demo.camel.yaml"), StandardCharsets.UTF_8));
// the standalone validation reports the same errors, for content and
for a file in the directory
JsonObject check = facade.validateSource("demo", "new.camel.yaml", "-
log:\n logLevel: WARN\n");
assertFalse(check.getBoolean("valid"));
assertEquals(1, check.getCollection("errors").size());
JsonObject fileCheck = facade.validateSource("demo",
"demo.camel.yaml", null);
- assertFalse(fileCheck.getBoolean("valid"), "the file now contains the
invalid content");
+ assertTrue(fileCheck.getBoolean("valid"), "the file still holds the
content it had before the refused write");
assertEquals("demo.camel.yaml", fileCheck.getString("file"));
assertTrue(facade.validateSource("demo", null, "- route:
{}").getBoolean("valid"), "content alone is YAML");
assertEquals("error", facade.validateSource("demo", "missing.yaml",
null).getString("status"));