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 ff0bdf925cc6 CAMEL-25445: camel-jbang - text from the integrations no
longer acts on the terminal
ff0bdf925cc6 is described below
commit ff0bdf925cc6c0d0f51a07d81a739759cc4daa30
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 14 12:30:53 2026 +0200
CAMEL-25445: camel-jbang - text from the integrations no longer acts on the
terminal
The TUI shows what the integrations send it: log lines, message bodies and
headers, exceptions, span attributes. TamboUI turns a C0 control character
into a space, but passed a C1 control character (U+0080 to U+009F) on to
the terminal, where the browser terminal of --web acts on it, and a
terminal that lays out bidirectional text acted on the BiDi controls.
After each frame is drawn, ScreenSanitizer now cleans every cell, whichever
view drew it: a control character in a cell of its own becomes a space, and
a control, BiDi or invisible format character joined to a cell is dropped.
ZWJ, ZWNJ and the variation selectors stay, as emoji and some scripts need
them. The Developer tools section of the security model says so.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../modules/ROOT/pages/security-model.adoc | 10 +-
dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md | 1 +
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 2 +-
.../jbang/core/commands/tui/ScreenSanitizer.java | 90 +++++++++++++++++
.../core/commands/tui/ScreenSanitizerTest.java | 112 +++++++++++++++++++++
5 files changed, 211 insertions(+), 4 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/security-model.adoc
b/docs/user-manual/modules/ROOT/pages/security-model.adoc
index 67df8d7d7a4d..5a7f95013bd6 100644
--- a/docs/user-manual/modules/ROOT/pages/security-model.adoc
+++ b/docs/user-manual/modules/ROOT/pages/security-model.adoc
@@ -132,7 +132,10 @@ on the developer's own machine, for the developer's own
integrations, in the
inner loop of writing and trying routes; they are not part of a production
deployment. The developer is fully trusted, as a route author and an operator
are, and so is the local machine: its user account, its loopback interface and
-the processes the developer starts.
+the processes the developer starts. What the integrations send the TUI is not:
+log lines, message bodies and headers, exceptions and span attributes carry
+data from external message senders, and the TUI shows them without the
+characters a terminal would act on (control and BiDi characters).
Camel TUI has AI assistants: the model of the AI panel (a local or a hosted
LLM), a coding agent driving the panel over ACP, and an MCP client connected
@@ -163,8 +166,9 @@ deliberately few, and they are the contract:
A tool that breaks this contract is in scope: for example a file written
outside the source directory, a message sent to an endpoint the integration
-does not have without the developer's confirmation, or a key sent over MCP
-that answers a confirmation. An assistant doing what the tools allow is the
+does not have without the developer's confirmation, a key sent over MCP that
+answers a confirmation, or text from an integration that acts on the
+terminal. An assistant doing what the tools allow is the
trusted developer's case, also when untrusted data shown in the TUI - a
message body, a log line, an example's README - steered the model to it
(prompt injection); see _Out of scope_.
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
index 02ec79058eba..376e1fc47290 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/AGENTS.md
@@ -42,6 +42,7 @@ and use each other's package-private members, so there are no
sub-packages. Clas
| `Theme`, `TuiIcons` | colours (themes in `src/main/resources/tui/themes`)
and icons |
| `TuiSettings`, `SettingsPopup` | persisted settings |
| `TuiHelper` | shared helpers: text width, wrapping and truncation, ANSI,
number and duration formats, footer hints |
+| `ScreenSanitizer` | cleans every drawn frame of what a terminal would act
on: control and BiDi characters in text from the integrations |
| `RecordingManager`, `TapePlayer`, `TapeRecorder` | `--record` and tapes |
| `TuiWebServer` | `--web`, the terminal in a browser
(`src/main/resources/tui/web`) |
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 055aa71e488e..834cc9312110 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -1023,7 +1023,7 @@ public class CamelMonitor extends CamelCommand {
}
tui.run(
this::handleEvent,
- this::render);
+ frame -> ScreenSanitizer.render(frame, this::render));
} finally {
shellPanel.destroy();
aiPanel.destroy();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
new file mode 100644
index 000000000000..ce2c28a5b5e8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizer.java
@@ -0,0 +1,90 @@
+/*
+ * 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.camel.dsl.jbang.core.commands.tui;
+
+import java.util.function.Consumer;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+
+/**
+ * Keeps the text the integrations send from acting on the terminal
(CAMEL-25445). The TUI shows log lines, message
+ * bodies and headers, exceptions and span attributes as they come. TamboUI
turns a C0 control into a space, but hands a
+ * C1 control (U+0080 to U+009F) to the terminal, and the browser terminal of
{@code --web} acts on it, U+009B as CSI; a
+ * terminal that lays out bidirectional text reorders what follows a BiDi
control. So after a frame is drawn every cell
+ * is cleaned, whichever tab or popup drew it: a control character in a cell
of its own becomes a space, as TamboUI does
+ * for C0, and a control, a BiDi control or an invisible format character
joined to a cell is dropped. The joiners and
+ * selectors that emoji and some scripts need (ZWJ, ZWNJ, the variation
selectors) stay.
+ */
+final class ScreenSanitizer {
+
+ private ScreenSanitizer() {
+ }
+
+ /** Draws a frame, then cleans it. */
+ static void render(Frame frame, Consumer<Frame> draw) {
+ draw.accept(frame);
+ sanitize(frame.buffer());
+ }
+
+ static void sanitize(Buffer buffer) {
+ Rect area = buffer.area();
+ for (int y = area.top(); y < area.bottom(); y++) {
+ for (int x = area.left(); x < area.right(); x++) {
+ Cell cell = buffer.get(x, y);
+ String symbol = cell.symbol();
+ String clean = clean(symbol);
+ if (!clean.equals(symbol)) {
+ buffer.set(x, y, cell.symbol(clean));
+ }
+ }
+ }
+ }
+
+ /** The symbol of a cell without what a terminal would act on; the symbol
itself when there is nothing. */
+ static String clean(String symbol) {
+ if (symbol.isEmpty() || symbol.length() == 1 && symbol.charAt(0) >=
0x20 && symbol.charAt(0) < 0x7F) {
+ return symbol;
+ }
+ if (symbol.codePoints().noneMatch(ScreenSanitizer::unsafe)) {
+ return symbol;
+ }
+ StringBuilder clean = new StringBuilder(symbol.length());
+ int[] codePoints = symbol.codePoints().toArray();
+ for (int i = 0; i < codePoints.length; i++) {
+ if (!unsafe(codePoints[i])) {
+ clean.appendCodePoint(codePoints[i]);
+ } else if (i == 0) {
+ // the cell keeps its width
+ clean.append(' ');
+ }
+ }
+ return clean.toString();
+ }
+
+ static boolean unsafe(int cp) {
+ boolean control = cp < 0x20 || cp >= 0x7F && cp <= 0x9F;
+ // marks, embeddings, overrides and isolates
+ boolean bidi = cp == 0x061C || cp == 0x200E || cp == 0x200F || cp >=
0x202A && cp <= 0x202E
+ || cp >= 0x2066 && cp <= 0x2069;
+ // zero-width space, word joiner, byte order mark, soft hyphen,
Mongolian vowel separator
+ boolean invisible = cp == 0x200B || cp == 0x2060 || cp == 0xFEFF || cp
== 0x00AD || cp == 0x180E;
+ return control || bidi || invisible;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
new file mode 100644
index 000000000000..b9a04ae5d0c5
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ScreenSanitizerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.buffer.Cell;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Text from the integrations does not act on the terminal (CAMEL-25445): what
a terminal would take as a control is
+ * cleaned out of every cell after a frame is drawn.
+ */
+class ScreenSanitizerTest {
+
+ private static Buffer line(String text) {
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 60, 1));
+ buffer.setString(0, 0, text, Style.EMPTY);
+ return buffer;
+ }
+
+ private static List<String> symbols(Buffer buffer) {
+ List<String> symbols = new ArrayList<>();
+ Rect area = buffer.area();
+ for (int x = area.left(); x < area.right(); x++) {
+ symbols.add(buffer.get(x, 0).symbol());
+ }
+ return symbols;
+ }
+
+ private static String text(Buffer buffer) {
+ return TuiTestHelper.bufferToString(buffer).stripTrailing();
+ }
+
+ @Test
+ void aC1ControlBecomesASpaceInItsCell() {
+ // U+009B is CSI in one character: the browser terminal of --web would
colour what follows red
+ Buffer buffer = line("plain \u009b31mRED\u009b0m after");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).noneMatch(s ->
s.codePoints().anyMatch(ScreenSanitizer::unsafe));
+ // the parameters show as text, in the cells they had
+ assertThat(text(buffer)).isEqualTo("plain 31mRED 0m after");
+ }
+
+ @Test
+ void aBiDiControlOrAnInvisibleCharacterJoinedToACellIsDropped() {
+ Buffer buffer = line("bidi \u202eDESREVER\u202c end,
ad\u200bmin\ufeff");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).noneMatch(s ->
s.codePoints().anyMatch(ScreenSanitizer::unsafe));
+ assertThat(text(buffer)).isEqualTo("bidi DESREVER end, admin");
+ }
+
+ @Test
+ void aCellSetWithAControlCharacterBecomesASpace() {
+ // a widget can set a cell directly, which TamboUI does not clean, not
even C0
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 3, 1));
+ buffer.set(0, 0, new Cell("\u001b", Style.EMPTY));
+ buffer.set(1, 0, new Cell("\u009d", Style.EMPTY));
+ assertThat(symbols(buffer)).startsWith("\u001b", "\u009d");
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).startsWith(" ", " ");
+ }
+
+ @Test
+ void emojiAndScriptsStayWhole() {
+ // the joiners and selectors emoji and some scripts need: ZWJ,
variation selector 16, ZWNJ
+ Buffer buffer = line("👩\u200D💻 🇫🇷 ❤\uFE0F می\u200Cخواهم");
+ List<String> before = symbols(buffer);
+
+ ScreenSanitizer.sanitize(buffer);
+
+ assertThat(symbols(buffer)).isEqualTo(before);
+ assertThat(String.join("", symbols(buffer))).contains("\u200D",
"\uFE0F", "\u200C");
+ }
+
+ @Test
+ void theFrameIsCleanedAfterItIsDrawn() {
+ Buffer buffer = Buffer.empty(new Rect(0, 0, 10, 1));
+
+ ScreenSanitizer.render(Frame.forTesting(buffer), f ->
f.buffer().setString(0, 0, "\u009b2J ok", Style.EMPTY));
+
+ assertThat(text(buffer)).isEqualTo(" 2J ok");
+ assertThat(ScreenSanitizer.clean("a")).isSameAs("a");
+ }
+}