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.git

commit 9205e53fc0e69b056ccc38a9e4a50e2ecf40f2d5
Author: Luigi De Masi <[email protected]>
AuthorDate: Sat Oct 10 12:39:28 2026 +0200

    CAMEL-25499: Add opt-in semantic audit input snapshots
    
    Retain the selected state per expert with bounded immutable snapshots and 
an optional registry redactor. Keep captured input out of observer callbacks 
and report omissions without affecting evaluation outcomes. Expose the policy 
through YAML, XML and Java with schema, persistence and isolation regressions.
    
    Co-authored-by: OpenAI Codex <[email protected]>
---
 .../camel/catalog/docs/semantic-language.adoc      |  98 +++++-
 .../camel/catalog/schemas/camelYamlDsl-model.json  |  16 +
 .../src/main/docs/semantic-language.adoc           |  98 +++++-
 .../camel/language/semantic/SemanticLanguage.java  |   3 +
 .../camel/semantic/SemanticAuditConfiguration.java |  25 +-
 .../semantic/SemanticAuditInputConfiguration.java  |  65 ++++
 .../camel/semantic/SemanticAuditInputRedactor.java |  27 ++
 .../apache/camel/semantic/SemanticAuditRecord.java |  21 +-
 .../semantic/SemanticXmlRoutesBuilderLoader.java   |  16 +-
 .../internal/SemanticAuditInputSnapshot.java       |  88 +++++
 .../semantic/internal/SemanticAuditService.java    |  63 +++-
 .../yaml/SemanticDefinitionDeserializer.java       |  34 +-
 .../camel/semantic/SemanticAuditInputTest.java     | 355 +++++++++++++++++++++
 design/semantic-audit.adoc                         |  22 +-
 .../resources/schema/camelYamlDsl-canonical.json   |  18 ++
 .../resources/schema/camelYamlDsl-model.json       |  16 +
 .../generated/resources/schema/camelYamlDsl.json   |  18 ++
 .../camel/dsl/yaml/SemanticEvaluationTest.java     |  16 +-
 18 files changed, 963 insertions(+), 36 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 37c8f1cb7abb..34f6e30089b1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -1138,13 +1138,97 @@ Decision records carry explicit action, operation, 
target, optional namespace/co
 and policy identifiers, reason code and evidence IDs. They never derive 
application
 correlation or tenant identity from arbitrary inbound headers.
 
-Input, body, headers, variables, arbitrary provider metadata and exception 
messages are
-omitted before dispatch to any backend or observer. Caller-authored score 
descriptions
-are also omitted; the UI does not substitute today's definition for missing 
historical
-semantics. Strings are limited to 256 code points and collections to 100 
entries.
-Oversized result snapshots are omitted with `resultOmitted: snapshot_limit`, 
preserving
-the execution status without displaying altered labels or partial 
probabilities. These records are evidence about
-an evaluation and declared action, not a copy of the submitted content.
+Input is omitted by default. Headers, variables, arbitrary provider metadata 
and exception
+messages are never copied automatically. Caller-authored score descriptions 
are also omitted;
+static capability meanings remain available. Result and metadata strings are 
limited to
+256 code points, and their collections to 100 entries. An oversized result is 
represented by
+`resultOmitted: snapshot_limit` without changing the evaluation outcome.
+
+==== Storing evaluation input
+
+Enable input capture explicitly for each expert whose submitted state should 
be retained:
+
+[source,yaml]
+----
+- semantic:
+    audit:
+      enabled: true
+      experts:
+        security:
+          enabled: true
+          input:
+            enabled: true
+            maxChars: 4096
+            # redactor: auditInputRedactor
+        decisions:
+          enabled: false
+      sinks: [memory]
+      reader: memory
+----
+
+`input.enabled` defaults to `false` and does not enable auditing itself. It 
captures the
+selected state passed to the expert (`state`, including a selected header), 
before provider
+validation or execution. It does not take a copy of the entire exchange. 
Named, batch, direct
+and console evaluations use the same settings. Decisions link to their 
evaluation records;
+input is stored on the evaluation rather than duplicated on the decision.
+
+`maxChars` defaults to 4096 and accepts 1 through 100000. It bounds the total 
Unicode code
+points in strings, map keys and scalar representations. Snapshots accept text, 
JSON-compatible
+maps/lists and scalar values, with at most eight nested levels and 1000 nodes 
(including map
+keys). They are immutable; later changes to the submitted state cannot change 
history.
+Oversized, cyclic or unsupported input is omitted with
+`inputOmitted: snapshot_limit_or_unsupported_type`. Input is never truncated, 
and streams or
+arbitrary objects are not converted to text. `inputOmitted: unavailable` means 
the selected
+state was not obtained, for example because the state expression failed.
+
+An optional `redactor` names a registry bean implementing 
`SemanticAuditInputRedactor`.
+It receives a bounded, immutable copy on the evaluation thread and returns a 
sanitized value.
+The return value is bounded again; a null return omits input (`inputOmitted: 
redacted`).
+Successful redaction adds `inputRedacted: true`. Exceptions or invalid 
redactor output omit
+input (`inputOmitted: redaction_failed`) without recording exception text or 
affecting the
+expert result. Redactors must be thread-safe and should not block. Unknown 
bean references
+fail startup. For example, a redactor can remove confidential fields from 
structured state:
+
+[source,java]
+----
+context.getRegistry().bind("auditInputRedactor", (SemanticAuditInputRedactor) 
input -> {
+    if (input instanceof Map<?, ?> state) {
+        return Map.of("question", state.get("question"));
+    }
+    return null;
+});
+----
+
+Java uses the additional input settings map in `SemanticAuditConfiguration`:
+
+[source,java]
+----
+new SemanticAuditConfiguration(true, Map.of("security", true, "decisions", 
false),
+    List.of("memory"), "memory", 1000, 1000,
+    Map.of("security", new SemanticAuditInputConfiguration(true, 4096, 
"auditInputRedactor")));
+----
+
+The equivalent XML expert entry is:
+
+[source,xml]
+----
+<expert name="security" enabled="true" inputEnabled="true"
+        inputMaxChars="4096" inputRedactor="auditInputRedactor"/>
+----
+
+Input settings follow the same restart requirement as the other audit settings.
+Captured input goes to every configured audit sink, including `log` when 
selected, and is
+available through the audit reader, CLI, TUI and MCP. Existing backend 
retention and access
+controls therefore also apply to input. No automatic redaction is applied 
without a configured
+redactor. Input is never passed to `SemanticObserver`, even when audit input 
capture is enabled;
+auditing and telemetry remain independent. Existing records without input 
remain readable.
+
+The TUI inspector displays *Input* (or *Input (redacted)*) in the evaluation 
details, including
+linked evidence beside a route decision. It displays an omission reason when 
available, or
+*not captured* for records without input. The CLI prints it with
+`camel semantic audit my-app --event-id=<event-id>`; add `--json` for the 
stored structure.
+
+==== Observation hooks
 
 Register `SemanticObserver` beans to consume start/completion and explicit 
decision
 hooks independently of auditing. Beans must be registered before context 
startup: observers are
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 7472732229c7..47209ce3178f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -20573,6 +20573,22 @@
         "name" : "enabled",
         "type" : "boolean",
         "required" : true
+      }, {
+        "name" : "input",
+        "type" : "object",
+        "ref" : "semanticDefinitionDeserializer$AuditInputSchema"
+      } ]
+    },
+    "semanticDefinitionDeserializer$AuditInputSchema" : {
+      "children" : [ {
+        "name" : "enabled",
+        "type" : "boolean"
+      }, {
+        "name" : "maxChars",
+        "type" : "integer"
+      }, {
+        "name" : "redactor",
+        "type" : "string"
       } ]
     },
     "semanticDefinitionDeserializer$AuditSchema" : {
diff --git 
a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc 
b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 37c8f1cb7abb..34f6e30089b1 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -1138,13 +1138,97 @@ Decision records carry explicit action, operation, 
target, optional namespace/co
 and policy identifiers, reason code and evidence IDs. They never derive 
application
 correlation or tenant identity from arbitrary inbound headers.
 
-Input, body, headers, variables, arbitrary provider metadata and exception 
messages are
-omitted before dispatch to any backend or observer. Caller-authored score 
descriptions
-are also omitted; the UI does not substitute today's definition for missing 
historical
-semantics. Strings are limited to 256 code points and collections to 100 
entries.
-Oversized result snapshots are omitted with `resultOmitted: snapshot_limit`, 
preserving
-the execution status without displaying altered labels or partial 
probabilities. These records are evidence about
-an evaluation and declared action, not a copy of the submitted content.
+Input is omitted by default. Headers, variables, arbitrary provider metadata 
and exception
+messages are never copied automatically. Caller-authored score descriptions 
are also omitted;
+static capability meanings remain available. Result and metadata strings are 
limited to
+256 code points, and their collections to 100 entries. An oversized result is 
represented by
+`resultOmitted: snapshot_limit` without changing the evaluation outcome.
+
+==== Storing evaluation input
+
+Enable input capture explicitly for each expert whose submitted state should 
be retained:
+
+[source,yaml]
+----
+- semantic:
+    audit:
+      enabled: true
+      experts:
+        security:
+          enabled: true
+          input:
+            enabled: true
+            maxChars: 4096
+            # redactor: auditInputRedactor
+        decisions:
+          enabled: false
+      sinks: [memory]
+      reader: memory
+----
+
+`input.enabled` defaults to `false` and does not enable auditing itself. It 
captures the
+selected state passed to the expert (`state`, including a selected header), 
before provider
+validation or execution. It does not take a copy of the entire exchange. 
Named, batch, direct
+and console evaluations use the same settings. Decisions link to their 
evaluation records;
+input is stored on the evaluation rather than duplicated on the decision.
+
+`maxChars` defaults to 4096 and accepts 1 through 100000. It bounds the total 
Unicode code
+points in strings, map keys and scalar representations. Snapshots accept text, 
JSON-compatible
+maps/lists and scalar values, with at most eight nested levels and 1000 nodes 
(including map
+keys). They are immutable; later changes to the submitted state cannot change 
history.
+Oversized, cyclic or unsupported input is omitted with
+`inputOmitted: snapshot_limit_or_unsupported_type`. Input is never truncated, 
and streams or
+arbitrary objects are not converted to text. `inputOmitted: unavailable` means 
the selected
+state was not obtained, for example because the state expression failed.
+
+An optional `redactor` names a registry bean implementing 
`SemanticAuditInputRedactor`.
+It receives a bounded, immutable copy on the evaluation thread and returns a 
sanitized value.
+The return value is bounded again; a null return omits input (`inputOmitted: 
redacted`).
+Successful redaction adds `inputRedacted: true`. Exceptions or invalid 
redactor output omit
+input (`inputOmitted: redaction_failed`) without recording exception text or 
affecting the
+expert result. Redactors must be thread-safe and should not block. Unknown 
bean references
+fail startup. For example, a redactor can remove confidential fields from 
structured state:
+
+[source,java]
+----
+context.getRegistry().bind("auditInputRedactor", (SemanticAuditInputRedactor) 
input -> {
+    if (input instanceof Map<?, ?> state) {
+        return Map.of("question", state.get("question"));
+    }
+    return null;
+});
+----
+
+Java uses the additional input settings map in `SemanticAuditConfiguration`:
+
+[source,java]
+----
+new SemanticAuditConfiguration(true, Map.of("security", true, "decisions", 
false),
+    List.of("memory"), "memory", 1000, 1000,
+    Map.of("security", new SemanticAuditInputConfiguration(true, 4096, 
"auditInputRedactor")));
+----
+
+The equivalent XML expert entry is:
+
+[source,xml]
+----
+<expert name="security" enabled="true" inputEnabled="true"
+        inputMaxChars="4096" inputRedactor="auditInputRedactor"/>
+----
+
+Input settings follow the same restart requirement as the other audit settings.
+Captured input goes to every configured audit sink, including `log` when 
selected, and is
+available through the audit reader, CLI, TUI and MCP. Existing backend 
retention and access
+controls therefore also apply to input. No automatic redaction is applied 
without a configured
+redactor. Input is never passed to `SemanticObserver`, even when audit input 
capture is enabled;
+auditing and telemetry remain independent. Existing records without input 
remain readable.
+
+The TUI inspector displays *Input* (or *Input (redacted)*) in the evaluation 
details, including
+linked evidence beside a route decision. It displays an omission reason when 
available, or
+*not captured* for records without input. The CLI prints it with
+`camel semantic audit my-app --event-id=<event-id>`; add `--json` for the 
stored structure.
+
+==== Observation hooks
 
 Register `SemanticObserver` beans to consume start/completion and explicit 
decision
 hooks independently of auditing. Beans must be registered before context 
startup: observers are
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index 546e7d17e8f4..ea64debe3cf4 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -153,6 +153,7 @@ public class SemanticLanguage extends LanguageSupport {
             invocation = audit.begin(null, null, evaluation, operation, 
resolved.reference, resolved.capabilities.getProvider(),
                     null, request);
             reason = "invalid_input";
+            invocation.captureInput(input);
             operation.validateInput(input);
             resolved.provider.validateInput(evaluation, input);
             if (Thread.currentThread().isInterrupted()) {
@@ -167,6 +168,7 @@ public class SemanticLanguage extends LanguageSupport {
         } catch (Exception | AssertionError failure) {
             if (invocation == null) {
                 invocation = audit.begin(null, null, evaluation, null, 
auditExpert(evaluation), null, null, request);
+                invocation.captureInput(input);
             }
             invocation.complete("failed", failure instanceof 
InterruptedException ? "interrupted" : reason, null);
             if (failure instanceof InterruptedException) {
@@ -720,6 +722,7 @@ public class SemanticLanguage extends LanguageSupport {
                 active.addAll(invocations.values());
                 failureReason = "invalid_input";
                 Object state = current.state.evaluate(exchange, Object.class);
+                invocations.values().forEach(invocation -> 
invocation.captureInput(state));
                 // A state selector can itself evaluate another semantic 
expression.
                 exchange.removeProperty(RESULT);
                 exchange.removeProperty(RESULTS);
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditConfiguration.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditConfiguration.java
index c9818d90c86d..f93f65da4d58 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditConfiguration.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditConfiguration.java
@@ -24,6 +24,7 @@ import java.util.Objects;
 public final class SemanticAuditConfiguration {
     public static final SemanticAuditConfiguration DISABLED
             = new SemanticAuditConfiguration(false, Map.of(), 
List.of("memory"), "memory", 1000, 1000);
+    private final Map<String, SemanticAuditInputConfiguration> inputs;
     private final boolean enabled;
     private final Map<String, Boolean> experts;
     private final List<String> sinks;
@@ -33,6 +34,16 @@ public final class SemanticAuditConfiguration {
 
     public SemanticAuditConfiguration(boolean enabled, Map<String, Boolean> 
experts, List<String> sinks,
                                       String reader, int capacity, int 
queueCapacity) {
+        this(enabled, experts, sinks, reader, capacity, queueCapacity, 
Map.of());
+    }
+
+    public SemanticAuditConfiguration(boolean enabled, Map<String, Boolean> 
experts, List<String> sinks,
+                                      String reader, int capacity, int 
queueCapacity,
+                                      Map<String, 
SemanticAuditInputConfiguration> inputs) {
+        this.inputs = Map.copyOf(inputs);
+        if (!experts.keySet().containsAll(inputs.keySet())) {
+            throw new IllegalArgumentException("Audit input settings require a 
declared expert");
+        }
         this.enabled = enabled;
         this.experts = Map.copyOf(experts);
         this.sinks = List.copyOf(sinks);
@@ -68,6 +79,16 @@ public final class SemanticAuditConfiguration {
         return experts;
     }
 
+    public Map<String, SemanticAuditInputConfiguration> getInputs() {
+        return inputs;
+    }
+
+    public SemanticAuditInputConfiguration getInput(String expert) {
+        return expert == null
+                ? SemanticAuditInputConfiguration.DISABLED
+                : inputs.getOrDefault(expert, 
SemanticAuditInputConfiguration.DISABLED);
+    }
+
     public List<String> getSinks() {
         return sinks;
     }
@@ -88,11 +109,11 @@ public final class SemanticAuditConfiguration {
     public boolean equals(Object other) {
         return other instanceof SemanticAuditConfiguration c && enabled == 
c.enabled && experts.equals(c.experts)
                 && sinks.equals(c.sinks) && reader.equals(c.reader) && 
capacity == c.capacity
-                && queueCapacity == c.queueCapacity;
+                && queueCapacity == c.queueCapacity && inputs.equals(c.inputs);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(enabled, experts, sinks, reader, capacity, 
queueCapacity);
+        return Objects.hash(enabled, experts, sinks, reader, capacity, 
queueCapacity, inputs);
     }
 }
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputConfiguration.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputConfiguration.java
new file mode 100644
index 000000000000..bfe0e63e1189
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputConfiguration.java
@@ -0,0 +1,65 @@
+/*
+ * 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.semantic;
+
+import java.util.Objects;
+
+/** Per-expert opt-in to retain the selected evaluation input, independently 
of telemetry. */
+public final class SemanticAuditInputConfiguration {
+    public static final int DEFAULT_MAX_CHARS = 4096;
+    public static final int MAX_CHARS = 100000;
+    public static final SemanticAuditInputConfiguration DISABLED
+            = new SemanticAuditInputConfiguration(false, DEFAULT_MAX_CHARS, 
null);
+    private final boolean enabled;
+    private final int maxChars;
+    private final String redactor;
+
+    public SemanticAuditInputConfiguration(boolean enabled, int maxChars, 
String redactor) {
+        if (maxChars < 1 || maxChars > MAX_CHARS || redactor != null && 
redactor.isBlank()) {
+            throw new IllegalArgumentException(
+                    "Audit input requires maxChars between 1 and 100000 and a 
nonblank redactor reference");
+        }
+        this.enabled = enabled;
+        this.maxChars = maxChars;
+        this.redactor = redactor;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /** Total code points in strings, map keys and scalar representations; 
oversized input is omitted, not truncated. */
+    public int getMaxChars() {
+        return maxChars;
+    }
+
+    /** Optional registry name of a {@link SemanticAuditInputRedactor}. */
+    public String getRedactor() {
+        return redactor;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        return other instanceof SemanticAuditInputConfiguration c && enabled 
== c.enabled && maxChars == c.maxChars
+                && Objects.equals(redactor, c.redactor);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(enabled, maxChars, redactor);
+    }
+}
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputRedactor.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputRedactor.java
new file mode 100644
index 000000000000..ab86adbfbff6
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditInputRedactor.java
@@ -0,0 +1,27 @@
+/*
+ * 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.semantic;
+
+/** Optional registry bean applied to an immutable, bounded input snapshot 
before it reaches any audit sink. */
+@FunctionalInterface
+public interface SemanticAuditInputRedactor {
+    /**
+     * Return sanitized JSON-compatible data, or null to omit it. Called on 
the evaluation thread; implementations must
+     * be thread-safe and should not block. Throwing omits the input without 
changing the evaluation outcome.
+     */
+    Object redact(Object input) throws Exception;
+}
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
index 9cd7aeb71950..cd996b2beaa5 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticAuditRecord.java
@@ -23,9 +23,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.camel.semantic.internal.SemanticAuditInputSnapshot;
+
 /**
- * Immutable, size-bounded snapshot. It never contains the exchange, input or 
provider exception. Schema version 1 uses
- * UTC timestamps and keeps route actions separate from expert results.
+ * Immutable, size-bounded snapshot. It never contains the exchange or 
provider exception. Input is opt-in. Schema
+ * version 1 uses UTC timestamps and keeps route actions separate from expert 
results.
  */
 public final class SemanticAuditRecord {
     private final Map<String, Object> fields;
@@ -41,7 +43,8 @@ public final class SemanticAuditRecord {
         Set<String> keys = Set.of("schemaVersion", "eventId", "invocationId", 
"timestamp", "category", "origin", "requestId",
                 "batchId", "definition", "target", "expert", "operation", 
"contextId", "exchangeId", "routeId", "semantics",
                 "status", "reasonCode", "durationNanos", "resultOmitted", 
"result", "action", "namespace", "correlationId",
-                "policyId", "policyVersion", "rule", "evidence", "startedAt", 
"provider", "model", "revision");
+                "policyId", "policyVersion", "rule", "evidence", "startedAt", 
"provider", "model", "revision", "input",
+                "inputOmitted", "inputRedacted");
         if (!keys.containsAll(data.keySet()) || !(data.get("schemaVersion") 
instanceof Number version)
                 || version.doubleValue() != 1
                 || !(data.get("eventId") instanceof String id) || id.isBlank() 
|| id.length() > 256
@@ -50,7 +53,8 @@ public final class SemanticAuditRecord {
                 || !(data.get("timestamp") instanceof String timestamp)) {
             throw new IllegalArgumentException("Invalid semantic audit 
record");
         }
-        Set<String> structured = Set.of("schemaVersion", "durationNanos", 
"semantics", "result", "evidence");
+        Set<String> structured
+                = Set.of("schemaVersion", "durationNanos", "semantics", 
"result", "evidence", "input", "inputRedacted");
         data.forEach((key, value) -> {
             if (!structured.contains(key) && !(value instanceof String)) {
                 throw new IllegalArgumentException("Invalid audit text field: 
" + key);
@@ -100,8 +104,15 @@ public final class SemanticAuditRecord {
                 
probabilities.values().forEach(SemanticAuditRecord::validateProbability);
             }
         }
+        if (data.containsKey("input") && data.containsKey("inputOmitted")
+                || data.containsKey("inputRedacted") && 
(!(data.get("inputRedacted") instanceof Boolean)
+                        || !data.containsKey("input"))
+                || !"evaluation".equals(category) && 
(data.containsKey("input") || data.containsKey("inputOmitted"))) {
+            throw new IllegalArgumentException("Invalid audit input fields");
+        }
         Map<String, Object> copy = new LinkedHashMap<>();
-        data.forEach((key, value) -> copy.put(key, freeze(value, 0)));
+        data.forEach((key, value) -> copy.put(key, "input".equals(key)
+                ? SemanticAuditInputSnapshot.copy(value, 
SemanticAuditInputConfiguration.MAX_CHARS) : freeze(value, 0)));
         return new SemanticAuditRecord(copy);
     }
 
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
index 14d2015f5dda..bbc40b6f3e4d 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java
@@ -181,17 +181,27 @@ public class SemanticXmlRoutesBuilderLoader extends 
RoutesBuilderLoaderSupport {
     private SemanticAuditConfiguration audit(Element element) {
         attributes(element, Set.of("enabled", "reader", "capacity", 
"queueCapacity"));
         Map<String, Boolean> experts = new LinkedHashMap<>();
+        Map<String, SemanticAuditInputConfiguration> inputs = new 
LinkedHashMap<>();
         List<String> sinks = new ArrayList<>();
         for (Element child : children(element)) {
             if ("expert".equals(child.getLocalName())) {
-                attributes(child, Set.of("name", "enabled"));
-                String name = child.getAttribute("name");
+                attributes(child, Set.of("name", "enabled", "inputEnabled", 
"inputMaxChars", "inputRedactor"));
+                String name = 
getCamelContext().resolvePropertyPlaceholders(child.getAttribute("name"));
                 if (!child.hasAttribute("enabled")) {
                     throw new IllegalArgumentException("Audit expert '" + name 
+ "' requires enabled");
                 }
                 if (experts.putIfAbsent(name, 
auditBoolean(child.getAttribute("enabled"))) != null) {
                     throw new IllegalArgumentException("Duplicate audit 
expert: " + name);
                 }
+                if (child.hasAttribute("inputEnabled") || 
child.hasAttribute("inputMaxChars")
+                        || child.hasAttribute("inputRedactor")) {
+                    inputs.put(name, new SemanticAuditInputConfiguration(
+                            child.hasAttribute("inputEnabled") && 
auditBoolean(child.getAttribute("inputEnabled")),
+                            child.hasAttribute("inputMaxChars")
+                                    ? auditCapacity(child, "inputMaxChars")
+                                    : 
SemanticAuditInputConfiguration.DEFAULT_MAX_CHARS,
+                            auditAttribute(child, "inputRedactor", null)));
+                }
             } else if ("sink".equals(child.getLocalName())) {
                 attributes(child, Set.of("ref"));
                 
sinks.add(getCamelContext().resolvePropertyPlaceholders(child.getAttribute("ref")));
@@ -206,7 +216,7 @@ public class SemanticXmlRoutesBuilderLoader extends 
RoutesBuilderLoaderSupport {
                 element.hasAttribute("enabled") && 
auditBoolean(element.getAttribute("enabled")),
                 experts, sinks.isEmpty() ? List.of("memory") : sinks,
                 auditAttribute(element, "reader", "memory"), 
auditCapacity(element, "capacity"),
-                auditCapacity(element, "queueCapacity"));
+                auditCapacity(element, "queueCapacity"), inputs);
     }
 
     private int auditCapacity(Element element, String key) {
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditInputSnapshot.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditInputSnapshot.java
new file mode 100644
index 000000000000..5887e68639b1
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditInputSnapshot.java
@@ -0,0 +1,88 @@
+/*
+ * 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.semantic.internal;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Bounded JSON values only: never converts arbitrary objects or consumes 
streams. */
+public final class SemanticAuditInputSnapshot {
+    private int remaining;
+    private int nodes = 1000;
+
+    private SemanticAuditInputSnapshot(int maxChars) {
+        remaining = maxChars;
+    }
+
+    public static Object copy(Object input, int maxChars) {
+        return new SemanticAuditInputSnapshot(maxChars).value(input, 0);
+    }
+
+    private Object value(Object value, int depth) {
+        if (--nodes < 0 || depth > 8) {
+            throw new IllegalArgumentException("Input snapshot limit");
+        }
+        if (value == null || value instanceof Boolean) {
+            text(String.valueOf(value));
+            return value;
+        }
+        if (value instanceof String text) {
+            return text(text);
+        }
+        if (value instanceof Byte || value instanceof Short || value 
instanceof Integer || value instanceof Long
+                || value instanceof Float || value instanceof Double || value 
instanceof BigDecimal
+                || value instanceof BigInteger) {
+            if (value instanceof Double d && !Double.isFinite(d) || value 
instanceof Float f && !Float.isFinite(f)
+                    || value instanceof BigInteger integer && 
integer.bitLength() > remaining * 4
+                    || value instanceof BigDecimal decimal && 
decimal.precision() > remaining) {
+                throw new IllegalArgumentException("Input snapshot limit");
+            }
+            text(value.toString());
+            return value;
+        }
+        if (value instanceof List<?> list && list.size() <= nodes) {
+            List<Object> copy = new ArrayList<>();
+            for (Object item : list) {
+                copy.add(value(item, depth + 1));
+            }
+            return Collections.unmodifiableList(copy);
+        }
+        if (value instanceof Map<?, ?> map && map.size() <= nodes / 2) {
+            Map<String, Object> copy = new LinkedHashMap<>();
+            for (var entry : map.entrySet()) {
+                if (!(entry.getKey() instanceof String key)) {
+                    throw new IllegalArgumentException("Unsupported input 
key");
+                }
+                copy.put((String) value(key, depth + 1), 
value(entry.getValue(), depth + 1));
+            }
+            return Collections.unmodifiableMap(copy);
+        }
+        throw new IllegalArgumentException("Unsupported input or snapshot 
limit");
+    }
+
+    private String text(String text) {
+        if (text.length() > remaining * 2 || (remaining -= 
text.codePointCount(0, text.length())) < 0) {
+            throw new IllegalArgumentException("Input snapshot limit");
+        }
+        return text;
+    }
+}
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
index cf0577690468..097c7ff93f08 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/internal/SemanticAuditService.java
@@ -38,6 +38,8 @@ import org.apache.camel.builder.ThreadPoolBuilder;
 import org.apache.camel.semantic.LoggingSemanticAuditSink;
 import org.apache.camel.semantic.MemorySemanticAuditStore;
 import org.apache.camel.semantic.SemanticAuditConfiguration;
+import org.apache.camel.semantic.SemanticAuditInputConfiguration;
+import org.apache.camel.semantic.SemanticAuditInputRedactor;
 import org.apache.camel.semantic.SemanticAuditReader;
 import org.apache.camel.semantic.SemanticAuditRecord;
 import org.apache.camel.semantic.SemanticAuditSink;
@@ -64,6 +66,7 @@ public final class SemanticAuditService extends 
ServiceSupport {
     private final AtomicLong observerErrors = new AtomicLong();
     private final Map<String, AtomicLong> sinkErrors = new LinkedHashMap<>();
     private final Map<String, SemanticAuditSink> sinks = new LinkedHashMap<>();
+    private final Map<String, SemanticAuditInputRedactor> redactors = new 
LinkedHashMap<>();
     private volatile List<SemanticObserver> observers = List.of();
     private volatile ExecutorService executor;
     private MemorySemanticAuditStore memory;
@@ -148,6 +151,18 @@ public final class SemanticAuditService extends 
ServiceSupport {
         if (memory == null || memory.getCapacity() != 
configuration.getCapacity()) {
             memory = new MemorySemanticAuditStore(configuration.getCapacity());
         }
+        redactors.clear();
+        for (var entry : configuration.getInputs().entrySet()) {
+            String name = entry.getValue().getRedactor();
+            if (name != null) {
+                SemanticAuditInputRedactor redactor
+                        = context.getRegistry().lookupByNameAndType(name, 
SemanticAuditInputRedactor.class);
+                if (redactor == null) {
+                    throw new IllegalArgumentException("Unknown semantic audit 
input redactor: " + name);
+                }
+                redactors.put(entry.getKey(), redactor);
+            }
+        }
         sinkErrors.clear();
         for (String name : configuration.getSinks()) {
             SemanticAuditSink sink = switch (name) {
@@ -238,6 +253,11 @@ public final class SemanticAuditService extends 
ServiceSupport {
         status.put("enabled", configuration.isEnabled());
         status.put("decisionsEnabled", configuration.isCapturing());
         status.put("experts", configuration.getExperts());
+        Map<String, Object> inputs = new LinkedHashMap<>();
+        configuration.getInputs().forEach((expert, input) -> inputs.put(expert,
+                Map.of("enabled", input.isEnabled(), "maxChars", 
input.getMaxChars(),
+                        "redacted", input.getRedactor() != null)));
+        status.put("inputs", inputs);
         status.put("reader", configuration.getReader());
         status.put("sinks", configuration.getSinks());
         status.put("capacity", configuration.getCapacity());
@@ -361,10 +381,50 @@ public final class SemanticAuditService extends 
ServiceSupport {
         private final List<SemanticObserver.Observation> handles = new 
ArrayList<>();
         // Confined to the invocation thread; cancellation emits a separate 
request record.
         private boolean completed;
+        private final Map<String, Object> inputFields = new LinkedHashMap<>();
 
         private Invocation(Map<String, Object> fields, boolean enabled) {
             this.fields = fields;
             this.enabled = enabled;
+            if (enabled && configuration.getInput((String) 
fields.get("expert")).isEnabled()) {
+                inputFields.put("inputOmitted", "unavailable");
+            }
+        }
+
+        /** Snapshot the effective selected state before a provider can mutate 
it. Observers never receive this data. */
+        public void captureInput(Object input) {
+            if (fields == null || completed || inputFields.isEmpty()) {
+                return;
+            }
+            SemanticAuditInputConfiguration policy = 
configuration.getInput((String) fields.get("expert"));
+            SemanticAuditInputRedactor redactor = 
redactors.get(fields.get("expert"));
+            inputFields.clear();
+            if (policy.getRedactor() != null && redactor == null) {
+                inputFields.put("inputOmitted", "redaction_failed");
+                return;
+            }
+            Object snapshot;
+            try {
+                snapshot = SemanticAuditInputSnapshot.copy(input, 
policy.getMaxChars());
+            } catch (Exception | AssertionError | LinkageError failure) {
+                inputFields.put("inputOmitted", 
"snapshot_limit_or_unsupported_type");
+                return;
+            }
+            if (redactor != null) {
+                try {
+                    snapshot = redactor.redact(snapshot);
+                    if (snapshot == null) {
+                        inputFields.put("inputOmitted", "redacted");
+                        return;
+                    }
+                    snapshot = SemanticAuditInputSnapshot.copy(snapshot, 
policy.getMaxChars());
+                } catch (Exception | AssertionError | LinkageError failure) {
+                    inputFields.put("inputOmitted", "redaction_failed");
+                    return;
+                }
+                inputFields.put("inputRedacted", true);
+            }
+            inputFields.put("input", snapshot);
         }
 
         public String getEventId() {
@@ -404,7 +464,8 @@ public final class SemanticAuditService extends 
ServiceSupport {
                 }
             }
             if (enabled) {
-                publish(record);
+                fields.putAll(inputFields);
+                publish(inputFields.isEmpty() ? record : 
SemanticAuditRecord.fromMap(fields));
             }
         }
     }
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
index d09e5be2b555..c102da9d2e6b 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java
@@ -31,6 +31,7 @@ import 
org.apache.camel.dsl.yaml.common.YamlDeserializerSupport;
 import org.apache.camel.dsl.yaml.common.exception.InvalidNodeTypeException;
 import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
 import org.apache.camel.semantic.SemanticAuditConfiguration;
+import org.apache.camel.semantic.SemanticAuditInputConfiguration;
 import org.apache.camel.semantic.SemanticEvaluation;
 import org.apache.camel.semantic.SemanticEvaluationBuilder;
 import org.apache.camel.semantic.SemanticEvaluations;
@@ -181,13 +182,26 @@ public class SemanticDefinitionDeserializer extends 
YamlDeserializerSupport impl
             throw new YamlDeserializationException(node, "Unknown semantic 
audit option");
         }
         Map<String, Boolean> experts = new LinkedHashMap<>();
+        Map<String, SemanticAuditInputConfiguration> inputs = new 
LinkedHashMap<>();
         if (values.containsKey("experts")) {
             fields(values.get("experts"), "audit experts").forEach((name, 
expert) -> {
                 Map<String, Node> settings = fields(expert, "audit expert");
-                if (!settings.keySet().equals(Set.of("enabled"))) {
-                    throw new YamlDeserializationException(expert, "Audit 
expert requires only enabled");
+                if (!settings.containsKey("enabled") || !Set.of("enabled", 
"input").containsAll(settings.keySet())) {
+                    throw new YamlDeserializationException(expert, "Audit 
expert requires enabled and optional input");
                 }
                 experts.put(name, auditBoolean(context, 
settings.get("enabled")));
+                if (settings.containsKey("input")) {
+                    Map<String, Node> input = fields(settings.get("input"), 
"audit input");
+                    if (!Set.of("enabled", "maxChars", 
"redactor").containsAll(input.keySet())) {
+                        throw new 
YamlDeserializationException(settings.get("input"), "Unknown audit input 
option");
+                    }
+                    inputs.put(name, new SemanticAuditInputConfiguration(
+                            input.containsKey("enabled") && 
auditBoolean(context, input.get("enabled")),
+                            input.containsKey("maxChars")
+                                    ? auditCapacity(context, input, "maxChars")
+                                    : 
SemanticAuditInputConfiguration.DEFAULT_MAX_CHARS,
+                            auditText(context, input, "redactor", null)));
+                }
             });
         }
         List<String> sinks = values.containsKey("sinks")
@@ -197,7 +211,7 @@ public class SemanticDefinitionDeserializer extends 
YamlDeserializerSupport impl
         return new SemanticAuditConfiguration(
                 values.containsKey("enabled") && auditBoolean(context, 
values.get("enabled")),
                 experts, sinks, auditText(context, values, "reader", "memory"),
-                auditCapacity(context, values, "capacity"), 
auditCapacity(context, values, "queueCapacity"));
+                auditCapacity(context, values, "capacity"), 
auditCapacity(context, values, "queueCapacity"), inputs);
     }
 
     private static int auditCapacity(CamelContext context, Map<String, Node> 
values, String key) {
@@ -265,10 +279,22 @@ public class SemanticDefinitionDeserializer extends 
YamlDeserializerSupport impl
     public static class AuditSchema {
     }
 
-    @YamlType(properties = { @YamlProperty(name = "enabled", type = "boolean", 
required = true) })
+    @YamlType(properties = {
+            @YamlProperty(name = "enabled", type = "boolean", required = true),
+            @YamlProperty(name = "input",
+                          type = 
"object:org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditInputSchema")
+    })
     public static class AuditExpertSchema {
     }
 
+    @YamlType(properties = {
+            @YamlProperty(name = "enabled", type = "boolean"),
+            @YamlProperty(name = "maxChars", type = "integer"),
+            @YamlProperty(name = "redactor", type = "string")
+    })
+    public static class AuditInputSchema {
+    }
+
     private static SemanticEvaluation readEvaluation(
             CamelContext context, String name, String expert, Node definition, 
Map<String, Node> values) {
         String description = "semantic evaluation '" + name + "'";
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticAuditInputTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticAuditInputTest.java
new file mode 100644
index 000000000000..1deb2ee83e0d
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticAuditInputTest.java
@@ -0,0 +1,355 @@
+/*
+ * 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.semantic;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.language.semantic.SemanticLanguage;
+import org.apache.camel.semantic.internal.SemanticAuditInputSnapshot;
+import org.apache.camel.semantic.internal.SemanticAuditService;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.snakeyaml.engine.v2.api.LoadSettings;
+import org.snakeyaml.engine.v2.api.lowlevel.Compose;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SemanticAuditInputTest {
+    @Test
+    void captureIsPerExpertAndNeverSentToObservers() throws Exception {
+        try (var context = new DefaultCamelContext()) {
+            var expert = new FixedSemanticExpert();
+            context.getRegistry().bind("security", expert);
+            context.getRegistry().bind("decisions", expert);
+            AtomicInteger observed = new AtomicInteger();
+            context.getRegistry().bind("observer", new SemanticObserver() {
+                public Observation started(SemanticAuditRecord record, Object 
parent) {
+                    assertThat(record.toMap()).doesNotContainKeys("input", 
"inputOmitted", "inputRedacted");
+                    return completed -> {
+                        
assertThat(completed.toMap()).doesNotContainKeys("input", "inputOmitted", 
"inputRedacted");
+                        observed.incrementAndGet();
+                    };
+                }
+            });
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 4096, null)));
+            context.start();
+            var language = (SemanticLanguage) 
context.resolveLanguage("semantic");
+            String input = "first line\nsecond line " + "x".repeat(300);
+            language.evaluate(evaluation("security"), input);
+            language.evaluate(evaluation("decisions"), "private");
+            assertThatThrownBy(() -> audit.configure("test", 
configuration(true, SemanticAuditInputConfiguration.DISABLED)))
+                    .hasMessageContaining("restart");
+            audit.stop();
+            assertThat(records(audit)).hasSize(2);
+            
assertThat(records(audit).get(0).toMap()).doesNotContainKey("input");
+            assertThat(records(audit).get(1).toMap()).containsEntry("input", 
input);
+            assertThat(observed).hasValue(2);
+            assertThat(audit.status()).containsEntry("observerErrors", 0L);
+            assertThat(expert.states).containsExactly(input, "private");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void captureDoesNotEnableAuditingAndDefaultsToDisabled(boolean auditing) 
throws Exception {
+        try (var context = new DefaultCamelContext()) {
+            context.getRegistry().bind("security", new FixedSemanticExpert());
+            AtomicInteger redacted = new AtomicInteger();
+            context.getRegistry().bind("redactor", 
(SemanticAuditInputRedactor) input -> {
+                redacted.incrementAndGet();
+                return input;
+            });
+            context.getRegistry().bind("observer", new SemanticObserver() {
+                public Observation started(SemanticAuditRecord record, Object 
parent) {
+                    return completed -> 
assertThat(completed.toMap()).doesNotContainKey("input");
+                }
+            });
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(auditing, auditing
+                    ? SemanticAuditInputConfiguration.DISABLED
+                    : new SemanticAuditInputConfiguration(true, 100, 
"redactor")));
+            context.start();
+            ((SemanticLanguage) 
context.resolveLanguage("semantic")).evaluate(evaluation("security"), 
"private");
+            audit.stop();
+            assertThat(redacted).hasValue(0);
+            assertThat(records(audit)).hasSize(auditing ? 1 : 0)
+                    .allSatisfy(record -> 
assertThat(record.toMap()).doesNotContainKeys("input", "inputOmitted"));
+        }
+    }
+
+    @Test
+    void 
selectedStateIsCapturedBeforeProviderMutationAndLinkedThroughConsole() throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            context.getRegistry().bind("security", new StructuredExpert());
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 100, null)));
+            SemanticEvaluations.get(context).replace("definitions", Map.of(
+                    "a", new SemanticEvaluation("check", "security", 
"${header.selected}", Map.of()),
+                    "b", new SemanticEvaluation("check", "security", 
"${header.selected}", Map.of())));
+            context.start();
+            var exchange = new DefaultExchange(context);
+            Map<String, Object> input = new LinkedHashMap<>(Map.of("messages", 
new ArrayList<>(List.of("hello"))));
+            exchange.getMessage().setBody("not selected");
+            exchange.getMessage().setHeader("selected", input);
+            
context.resolveLanguage("semantic").createExpression("refs:a,b").evaluate(exchange,
 Map.class);
+            SemanticAuditDecision decision = new SemanticAuditDecision();
+            decision.setFields(Map.of("action", "allow"));
+            decision.process(exchange);
+            audit.stop();
+            assertThat(input).containsEntry("mutated", true);
+            assertThat(records(audit)).hasSize(3);
+            
assertThat(records(audit).get(0).toMap()).doesNotContainKey("input");
+            for (var record : records(audit).subList(1, 3)) {
+                assertThat(record.toMap()).containsEntry("input", 
Map.of("messages", List.of("hello")));
+                var restored = SemanticAuditRecord.fromMap((JsonObject) 
Jsoner.deserialize(Jsoner.serialize(record.toMap())));
+                assertThat(restored.toMap()).containsEntry("input", 
record.toMap().get("input"));
+                assertThatThrownBy(() -> ((Map<String, Object>) 
restored.toMap().get("input")).put("secret", "x"))
+                        .isInstanceOf(UnsupportedOperationException.class);
+            }
+            var console = new SemanticAuditConsole();
+            console.setCamelContext(context);
+            var response = (JsonObject) console.call(DevConsole.MediaType.JSON,
+                    Map.of("eventId", records(audit).get(0).getEventId()));
+            
assertThat(Jsoner.serialize(response)).contains("hello").doesNotContain("not 
selected", "mutated");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "replace", "omit", "throw", "oversize", "mutate" 
})
+    void redactionIsAppliedBeforeStorageAndFailsClosed(String behavior) throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            var expert = new StructuredExpert();
+            context.getRegistry().bind("security", expert);
+            context.getRegistry().bind("redactor", 
(SemanticAuditInputRedactor) input -> {
+                return switch (behavior) {
+                    case "replace" -> Map.of("prompt", "[redacted]");
+                    case "omit" -> null;
+                    case "throw" -> throw new IllegalStateException("secret 
diagnostic");
+                    case "mutate" -> ((Map<String, Object>) 
input).put("prompt", "changed");
+                    default -> "x".repeat(101);
+                };
+            });
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 100, "redactor")));
+            context.start();
+            Map<String, Object> input = new LinkedHashMap<>(Map.of("prompt", 
"secret"));
+            var result = ((SemanticLanguage) 
context.resolveLanguage("semantic"))
+                    .evaluate(new SemanticEvaluation("check", "security", 
null, Map.of()), input);
+            assertThat(result.getValue()).isEqualTo(true);
+            assertThat(input).containsEntry("prompt", "secret");
+            audit.stop();
+            var record = records(audit).get(0);
+            
assertThat(Jsoner.serialize(record.toMap())).doesNotContain("secret", 
"changed");
+            if (behavior.equals("replace")) {
+                assertThat(record.toMap()).containsEntry("input", 
Map.of("prompt", "[redacted]"))
+                        .containsEntry("inputRedacted", true);
+            } else {
+                
assertThat(record.toMap()).doesNotContainKey("input").containsEntry("inputOmitted",
+                        behavior.equals("omit") ? "redacted" : 
"redaction_failed");
+            }
+        }
+    }
+
+    @Test
+    void oversizedOrUnsupportedInputDoesNotChangeEvaluationOutcome() throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            context.getRegistry().bind("security", new StructuredExpert());
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 4, null)));
+            context.start();
+            var language = (SemanticLanguage) 
context.resolveLanguage("semantic");
+            for (Object input : List.of("too long", new 
LinkedHashMap<>(Map.of("x", new Object())))) {
+                assertThat(language.evaluate(new SemanticEvaluation("check", 
"security", null, Map.of()), input).getValue())
+                        .isEqualTo(true);
+            }
+            audit.stop();
+            assertThat(records(audit)).hasSize(2).allSatisfy(record -> 
assertThat(record.toMap()).doesNotContainKey("input")
+                    .containsEntry("inputOmitted", 
"snapshot_limit_or_unsupported_type"));
+        }
+    }
+
+    @Test
+    void 
snapshotsBoundCharactersDepthAndNodesWithoutConsumingOrStringifyingObjects() {
+        assertThat(SemanticAuditInputSnapshot.copy("😀\n", 2)).isEqualTo("😀\n");
+        assertThatThrownBy(() -> SemanticAuditInputSnapshot.copy("😀x", 
1)).isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> SemanticAuditInputSnapshot.copy(Double.NaN, 
100)).isInstanceOf(IllegalArgumentException.class);
+        List<Object> cycle = new ArrayList<>();
+        cycle.add(cycle);
+        assertThatThrownBy(() -> SemanticAuditInputSnapshot.copy(cycle, 
100)).isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> 
SemanticAuditInputSnapshot.copy(Collections.nCopies(1000, ""), 100))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> SemanticAuditInputSnapshot.copy(new Object() {
+            public String toString() {
+                throw new AssertionError("must not stringify");
+            }
+        }, 100)).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "yaml", "xml", "java" })
+    void allDslsConfigureInputAndFreezeItUntilRestart(String dsl) throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            context.getRegistry().bind("security", new FixedSemanticExpert());
+            context.getRegistry().bind("redactor", 
(SemanticAuditInputRedactor) input -> "safe");
+            if (dsl.equals("yaml")) {
+                yaml(context, "{enabled: true, maxChars: 42, redactor: 
redactor}");
+            } else if (dsl.equals("xml")) {
+                context.build();
+                
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("audit.xml",
 """
+                        <semantic 
xmlns="http://camel.apache.org/schema/semantic";>
+                          <audit enabled="true"><expert name="security" 
enabled="true"
+                              inputEnabled="true" inputMaxChars="42" 
inputRedactor="redactor"/></audit>
+                        </semantic>
+                        """));
+            } else {
+                context.addRoutes(new RouteBuilder() {
+                    public void configure() {
+                        SemanticEvaluationsBuilder.semanticEvaluations(this)
+                                
.audit(SemanticAuditInputTest.configuration(true,
+                                        new 
SemanticAuditInputConfiguration(true, 42, "redactor")))
+                                .register();
+                    }
+                });
+            }
+            context.start();
+            var audit = SemanticAuditService.get(context);
+            assertThat(audit.getConfiguration().getInput("security"))
+                    .isEqualTo(new SemanticAuditInputConfiguration(true, 42, 
"redactor"));
+            ((SemanticLanguage) 
context.resolveLanguage("semantic")).evaluate(evaluation("security"), "secret");
+            assertThatThrownBy(() -> audit.validateConfiguration("different",
+                    configuration(true, 
SemanticAuditInputConfiguration.DISABLED)))
+                    .isInstanceOf(IllegalArgumentException.class);
+            audit.stop();
+            assertThat(records(audit).get(0).toMap()).containsEntry("input", 
"safe");
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "{enabled: maybe}", "{enabled: true, maxChars: 
0}", "{maxChars: abc}", "{unknown: true}", "true" })
+    void invalidYamlInputSettingsIncludeSourcePosition(String input) throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            assertThatThrownBy(() -> yaml(context, 
input)).hasMessageContaining("audit.yaml");
+        }
+    }
+
+    @Test
+    void persistedRecordsRejectOversizedAndContradictoryInput() {
+        Map<String, Object> fields = new LinkedHashMap<>(
+                Map.of("schemaVersion", 1, "eventId", "event",
+                        "category", "evaluation", "timestamp", 
"2026-10-10T10:00:00Z"));
+        fields.put("input", 
"x".repeat(SemanticAuditInputConfiguration.MAX_CHARS + 1));
+        assertThatThrownBy(() -> 
SemanticAuditRecord.fromMap(fields)).isInstanceOf(IllegalArgumentException.class);
+        fields.put("input", "hello");
+        fields.put("inputOmitted", "redacted");
+        assertThatThrownBy(() -> 
SemanticAuditRecord.fromMap(fields)).isInstanceOf(IllegalArgumentException.class);
+        fields.remove("inputOmitted");
+        fields.put("category", "decision");
+        assertThatThrownBy(() -> 
SemanticAuditRecord.fromMap(fields)).isInstanceOf(IllegalArgumentException.class);
+        fields.put("category", "evaluation");
+        fields.put("inputRedacted", "yes");
+        assertThatThrownBy(() -> 
SemanticAuditRecord.fromMap(fields)).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void invocationBeforeStartupNeverBypassesAConfiguredRedactor() throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            context.getRegistry().bind("redactor", 
(SemanticAuditInputRedactor) input -> "safe");
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 100, "redactor")));
+            var invocation = audit.begin(null, null, evaluation("security"), 
null, "security", null, null, null);
+            invocation.captureInput("private");
+            context.start();
+            invocation.complete("success", "evaluation_completed", null);
+            audit.stop();
+            
assertThat(records(audit).get(0).toMap()).doesNotContainKey("input").containsEntry("inputOmitted",
+                    "redaction_failed");
+        }
+    }
+
+    @Test
+    void unknownRedactorFailsStartup() throws Exception {
+        try (var context = new DefaultCamelContext()) {
+            var audit = SemanticAuditService.get(context);
+            audit.configure("test", configuration(true, new 
SemanticAuditInputConfiguration(true, 100, "missing")));
+            audit.activate();
+            assertThatThrownBy(context::start).hasMessageContaining("Unknown 
semantic audit input redactor: missing");
+        }
+    }
+
+    private static SemanticAuditConfiguration configuration(boolean enabled, 
SemanticAuditInputConfiguration input) {
+        return new SemanticAuditConfiguration(
+                enabled, Map.of("security", enabled, "decisions", enabled),
+                List.of("memory"), "memory", 100, 100, Map.of("security", 
input));
+    }
+
+    private static SemanticEvaluation evaluation(String expert) {
+        return new SemanticEvaluation("boolean", expert, null, Map.of());
+    }
+
+    private static List<SemanticAuditRecord> records(SemanticAuditService 
audit) throws Exception {
+        return audit.getReader().query(new SemanticAuditQuery(Map.of(), null, 
null, 200)).getRecords();
+    }
+
+    private static void yaml(DefaultCamelContext context, String input) throws 
Exception {
+        String yaml
+                = "- semantic:\n    audit:\n      enabled: true\n      
experts:\n        security:\n          enabled: true\n          input: "
+                  + input;
+        var settings = LoadSettings.builder().setLabel("audit.yaml").build();
+        try (var deserialization = new YamlDeserializationContext(settings)) {
+            deserialization.setCamelContext(context);
+            
deserialization.setResource(ResourceHelper.fromString("audit.yaml", yaml));
+            deserialization.start();
+            deserialization.preParse(new 
Compose(settings).composeString(yaml).orElseThrow());
+        }
+    }
+
+    @SemanticExpert(name = "structured", description = "Test", provider = 
"test", artifactId = "test",
+                    operations = @SemanticOperation(name = "check", 
description = "Check", inputTypes = {
+                            SemanticExpert.InputType.TEXT, 
SemanticExpert.InputType.STRUCTURED },
+                                                    inputRequirements = "Text 
or structured state",
+                                                    resultType = 
SemanticExpert.ResultType.BOOLEAN,
+                                                    resultMeaning = "Test 
result"))
+    static class StructuredExpert implements SemanticAdapter {
+        public void validate(SemanticEvaluation evaluation) {
+            SemanticCapabilities.from(getClass()).validate(evaluation);
+        }
+
+        public SemanticResult evaluate(SemanticEvaluation evaluation, Object 
state) {
+            if (state instanceof Map<?, ?> map) {
+                ((Map<String, Object>) map).put("mutated", true);
+            }
+            return new SemanticResult(true, null, null, null, Map.of());
+        }
+    }
+}
diff --git a/design/semantic-audit.adoc b/design/semantic-audit.adoc
index 79a3a604127a..3ae509947434 100644
--- a/design/semantic-audit.adoc
+++ b/design/semantic-audit.adoc
@@ -1,7 +1,7 @@
 = Semantic audit and observation proposal
 
-Status: proposed for review in CAMEL-25499 / PR #27649. This document 
describes the implementation being
-reviewed; it does not assert prior upstream design agreement.
+Status: the base implementation was merged in CAMEL-25499 / PR #27649. The 
input capture extension
+described below is proposed as a follow-up; it does not assert prior upstream 
design agreement.
 
 == Scope and related work
 
@@ -54,3 +54,21 @@ lookup. The TUI maintains its own browsing state. Its audit 
MCP tool queries the
 changing the screen, and is added to AI sessions only when the selected 
integration exposes audit history.
 The memory reader filters a consistent snapshot outside its append lock and 
binds cursors to a canonical
 SHA-256 query fingerprint. Backend failures expose a stable error code without 
backend messages.
+
+== Opt-in input capture
+
+The follow-up retains the selected evaluation state only when that expert 
explicitly enables
+`input.enabled`; auditing alone never enables input retention. Capture applies 
to direct, named,
+batch and console evaluations. A decision refers to its evaluation evidence 
rather than duplicating
+submitted input. Input is excluded from observer callbacks even when audit 
capture is enabled.
+
+The snapshot is taken before provider validation or execution and bounded by 
`maxChars` (4096 by
+default), depth and node count. Unsupported or oversized state is omitted with 
a stable reason,
+without truncation or arbitrary object conversion. An optional 
`SemanticAuditInputRedactor` registry
+bean receives the immutable snapshot; its output is bounded again. Redaction 
failure omits input
+without changing the evaluation result or retaining exception messages.
+
+Captured input follows the existing sink/reader contract and backend retention 
policy, including log
+output when the log sink is configured. It is available through the console, 
CLI, TUI and MCP. There
+is no separate input repository or telemetry payload path. Input settings use 
the same DSL authority
+and restart requirement as the other audit settings; existing records without 
input remain readable.
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 8a0cfe6ff2e1..1b75a14c0379 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -13619,10 +13619,28 @@
         "properties" : {
           "enabled" : {
             "type" : "boolean"
+          },
+          "input" : {
+            "$ref" : 
"#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditInputSchema"
           }
         },
         "required" : [ "enabled" ]
       },
+      
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditInputSchema"
 : {
+        "type" : "object",
+        "additionalProperties" : false,
+        "properties" : {
+          "enabled" : {
+            "type" : "boolean"
+          },
+          "maxChars" : {
+            "type" : "integer"
+          },
+          "redactor" : {
+            "type" : "string"
+          }
+        }
+      },
       
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditSchema" : {
         "type" : "object",
         "additionalProperties" : false,
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 7472732229c7..47209ce3178f 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -20573,6 +20573,22 @@
         "name" : "enabled",
         "type" : "boolean",
         "required" : true
+      }, {
+        "name" : "input",
+        "type" : "object",
+        "ref" : "semanticDefinitionDeserializer$AuditInputSchema"
+      } ]
+    },
+    "semanticDefinitionDeserializer$AuditInputSchema" : {
+      "children" : [ {
+        "name" : "enabled",
+        "type" : "boolean"
+      }, {
+        "name" : "maxChars",
+        "type" : "integer"
+      }, {
+        "name" : "redactor",
+        "type" : "string"
       } ]
     },
     "semanticDefinitionDeserializer$AuditSchema" : {
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index 2314c56c162b..8722a3fb7509 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -16519,10 +16519,28 @@
         "properties" : {
           "enabled" : {
             "type" : "boolean"
+          },
+          "input" : {
+            "$ref" : 
"#/items/definitions/org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditInputSchema"
           }
         },
         "required" : [ "enabled" ]
       },
+      
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditInputSchema"
 : {
+        "type" : "object",
+        "additionalProperties" : false,
+        "properties" : {
+          "enabled" : {
+            "type" : "boolean"
+          },
+          "maxChars" : {
+            "type" : "integer"
+          },
+          "redactor" : {
+            "type" : "string"
+          }
+        }
+      },
       
"org.apache.camel.semantic.yaml.SemanticDefinitionDeserializer$AuditSchema" : {
         "type" : "object",
         "additionalProperties" : false,
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
index 31609caf5096..8a5774ba4b4e 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticEvaluationTest.java
@@ -112,8 +112,9 @@ class SemanticEvaluationTest extends YamlTestSupport {
         }
     }
 
-    @Test
-    void auditSchemaAndDecisionBeanRecordOnlyTheEnabledExpert() throws 
Exception {
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void auditSchemaAndDecisionBeanRecordOnlyTheEnabledExpert(boolean 
captureInput) throws Exception {
         loadRoutes("""
                 - beans:
                   - name: security
@@ -132,8 +133,8 @@ class SemanticEvaluationTest extends YamlTestSupport {
                     audit:
                       enabled: true
                       experts:
-                        security: {enabled: true}
-                        decisions: {enabled: false}
+                        security: {enabled: true, input: {enabled: %s, 
maxChars: 4096}}
+                        decisions: {enabled: false, input: {enabled: true}}
                       sinks: [memory]
                       reader: memory
                       capacity: 10
@@ -161,7 +162,7 @@ class SemanticEvaluationTest extends YamlTestSupport {
                                   - process:
                                       ref: recordBlocked
                                   - to: mock:blocked
-                """);
+                """.formatted(captureInput));
         context.start();
         context.getEndpoint("mock:blocked", 
MockEndpoint.class).expectedMessageCount(1);
         try (var template = context.createProducerTemplate()) {
@@ -176,6 +177,11 @@ class SemanticEvaluationTest extends YamlTestSupport {
         assertThat(records.get(0).text("action")).isEqualTo("block");
         
assertThat(records.get(0).toMap().get("evidence")).isEqualTo(List.of(records.get(1).getEventId()));
         assertThat(records.get(1).getExpert()).isEqualTo("security");
+        if (captureInput) {
+            assertThat(records.get(1).toMap()).containsEntry("input", 
"injection attempt");
+        } else {
+            assertThat(records.get(1).toMap()).doesNotContainKey("input");
+        }
     }
 
     private static String declarations(String state) {

Reply via email to