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]
