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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12633-9ed8823c25aa760d057281ae542d38abf18ae12d
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit ea3d6df4a0721ecf9f42811a9cf3e00d82651700
Author: hutiefang76 <[email protected]>
AuthorDate: Thu Oct 8 13:35:35 2026 +0000

    [Fix][CLI] Redact credentials in conversation summaries (#12633)
---
 seatunnel-cli/README.md                    |  2 +
 seatunnel-cli/README.zh-CN.md              |  2 +
 seatunnel-cli/seatunnel_cli/memory.py      |  8 ++--
 seatunnel-cli/tests/test_memory_summary.py | 76 ++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 3 deletions(-)

diff --git a/seatunnel-cli/README.md b/seatunnel-cli/README.md
index dc315dd464..cf02b0e8cd 100644
--- a/seatunnel-cli/README.md
+++ b/seatunnel-cli/README.md
@@ -440,6 +440,8 @@ Transform metadata is resolved through the same path as 
source and sink metadata
 
 The CLI remembers facts across sessions to improve config accuracy:
 
+Conversation summaries use the same credential redaction as saved history: 
recognized secrets are removed before the summary request and before the 
summary is saved.
+
 - **Project context** -- Table names, database names, common patterns.
 - **Preferences** -- Parallelism, format, language preferences.
 
diff --git a/seatunnel-cli/README.zh-CN.md b/seatunnel-cli/README.zh-CN.md
index c4cc2e578b..2a8b0f36ad 100644
--- a/seatunnel-cli/README.zh-CN.md
+++ b/seatunnel-cli/README.zh-CN.md
@@ -408,6 +408,8 @@ Transform 元数据与 Source、Sink 元数据走同一条解析路径,因此
 
 CLI 跨会话记忆信息,以提高配置准确性:
 
+会话摘要采用与已保存历史相同的凭据脱敏规则:生成摘要的请求和保存的摘要都会移除已识别的敏感值。
+
 - **项目上下文** -- 表名、数据库名、常用模式。
 - **偏好设置** -- 并行度、格式、语言偏好。
 
diff --git a/seatunnel-cli/seatunnel_cli/memory.py 
b/seatunnel-cli/seatunnel_cli/memory.py
index f514f3685f..909a341469 100644
--- a/seatunnel-cli/seatunnel_cli/memory.py
+++ b/seatunnel-cli/seatunnel_cli/memory.py
@@ -309,7 +309,7 @@ class SessionManager:
         try:
             with open(path, "r", encoding="utf-8") as f:
                 data = json.load(f)
-            data["summary"] = summary
+            data["summary"] = redact_credentials(summary)
             _atomic_write(path, data)
         except Exception:
             pass
@@ -318,18 +318,20 @@ class SessionManager:
         if len(conversation_history) < 2:
             return ""
         snippets = []
-        for msg in conversation_history[:4] + conversation_history[-2:]:
+        summary_history = conversation_history[:4] + conversation_history[-2:]
+        for msg in _redact_conversation_history(summary_history):
             for block in msg.get("content", []):
                 if "text" in block:
                     snippets.append(f"{msg['role']}: {block['text'][:200]}")
         conversation_text = "\n".join(snippets)[:1500]
 
-        return client.quick_chat(
+        summary = client.quick_chat(
             f"Summarize this SeaTunnel conversation in one sentence (max 80 
chars, "
             f"language should match the conversation):\n\n{conversation_text}",
             system="Output ONLY the summary sentence, nothing else.",
             use_fast_model=True,
         ).strip()
+        return redact_credentials(summary)
 
 
 # ─── Memory Store ───
diff --git a/seatunnel-cli/tests/test_memory_summary.py 
b/seatunnel-cli/tests/test_memory_summary.py
new file mode 100644
index 0000000000..d45eb99ce1
--- /dev/null
+++ b/seatunnel-cli/tests/test_memory_summary.py
@@ -0,0 +1,76 @@
+#
+# 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.
+#
+
+"""Tests for credential redaction in conversation summaries."""
+
+import json
+from unittest.mock import Mock
+
+from seatunnel_cli.memory import SessionManager
+
+
+def 
test_summary_redacts_conversation_before_model_and_persisted_response(tmp_path):
+    manager = SessionManager(tmp_path)
+    history = [
+        {
+            "role": "user",
+            "content": [{"text": "Connect with password=exampleSecret123"}],
+        },
+        {
+            "role": "assistant",
+            "content": [{"text": "Use JDBC with password=exampleSecret123"}],
+        },
+    ]
+    client = Mock()
+    client.quick_chat.return_value = "JDBC password is responseSecret456"
+    manager.save_session(history)
+    summary = manager.generate_summary(history, client)
+    manager.update_summary(summary)
+
+    assert "exampleSecret123" not in client.quick_chat.call_args.args[0]
+    assert "responseSecret456" not in summary
+    saved = (manager.sessions_dir / 
f"{manager.current_session_id}.json").read_text()
+    assert "exampleSecret123" not in saved
+    assert "responseSecret456" not in saved
+    assert "REDACTED" in json.loads(saved)["summary"]
+    assert history[0]["content"][0]["text"].endswith("exampleSecret123")
+
+
+def 
test_direct_summary_update_redacts_credentials_and_preserves_other_fields(tmp_path):
+    manager = SessionManager(tmp_path)
+    manager.save_session([], "source { Jdbc { url=localhost } }")
+    manager.update_summary("JDBC password=directSecret789 at localhost")
+    data = json.loads(
+        (manager.sessions_dir / 
f"{manager.current_session_id}.json").read_text()
+    )
+    assert "directSecret789" not in data["summary"]
+    assert "localhost" in data["summary"]
+    assert data["last_config"] == "source { Jdbc { url=localhost } }"
+
+
+def test_summary_redacts_before_truncating_long_credential(tmp_path):
+    manager = SessionManager(tmp_path)
+    secret = "x" * 230
+    history = [
+        {"role": "user", "content": [{"text": "password=" + secret}]},
+        {"role": "assistant", "content": [{"text": "JDBC"}]},
+    ]
+    client = Mock()
+    client.quick_chat.return_value = "JDBC conversation"
+    manager.generate_summary(history, client)
+    assert "x" * 20 not in client.quick_chat.call_args.args[0]
+    assert "REDACTED" in client.quick_chat.call_args.args[0]

Reply via email to