From dbdd4ebec660fbe52f296d36f3bdd1483934d867 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Wed, 22 Jul 2026 01:57:59 -0400 Subject: [PATCH 1/2] fix(tool_runner): avoid leaking tool exception details to LLM (CWE-209) Salvage of RobotecAI/rai#814 by @sebastiondev. Exception messages raised by tool implementations were embeddedverbatim into ToolMessage.content returned to the agent. Log full details via logger.exception server-side and return only the tool name plus exception class name to the LLM/user. Adds offline regression tests proving secrets and hosts are not presented in ToolMessage content. Signed-off-by: Bartok9 --- .../rai/agents/langchain/core/tool_runner.py | 10 ++- .../langchain/test_tool_runner_cwe209.py | 90 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/agents/langchain/test_tool_runner_cwe209.py diff --git a/src/rai_core/rai/agents/langchain/core/tool_runner.py b/src/rai_core/rai/agents/langchain/core/tool_runner.py index 216748e2d..62cbc4760 100644 --- a/src/rai_core/rai/agents/langchain/core/tool_runner.py +++ b/src/rai_core/rai/agents/langchain/core/tool_runner.py @@ -105,9 +105,15 @@ def run_one(call: ToolCall): status="error", ) except Exception as e: - self.logger.info(f'Error in "{call["name"]}", error: {e}') + # Log full details server-side; do not leak exception text to the LLM/user (CWE-209). + self.logger.exception( + 'Error in "%s"', call["name"] + ) output = ToolMessage( - content=f"Failed to run tool. Error: {e}", + content=( + f"Tool '{call['name']}' failed with {type(e).__name__}. " + "Please try again or rephrase your request." + ), name=call["name"], tool_call_id=call["id"], status="error", diff --git a/tests/agents/langchain/test_tool_runner_cwe209.py b/tests/agents/langchain/test_tool_runner_cwe209.py new file mode 100644 index 000000000..e2f1dff58 --- /dev/null +++ b/tests/agents/langchain/test_tool_runner_cwe209.py @@ -0,0 +1,90 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed 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. + +"""Offline regression tests for ToolRunner CWE-209 exception sanitization.""" + +from logging import Logger +from unittest.mock import MagicMock + +from langchain_core.messages import AIMessage, ToolMessage +from langchain_core.tools import tool +from rai.agents.langchain.core.tool_runner import ToolRunner + + +@tool +def leaky_tool(x: str) -> str: + """A tool that raises with a sensitive message.""" + raise RuntimeError( + "DB connection failed: postgres://admin:SUPER_SECRET_PW@10.0.0.5:5432/prod" + ) + + +def test_tool_runner_does_not_leak_exception_details_to_llm(): + logger = MagicMock(spec=Logger) + runner = ToolRunner(tools=[leaky_tool], logger=logger) + state = { + "messages": [ + AIMessage( + content="", + tool_calls=[ + { + "name": "leaky_tool", + "args": {"x": "hi"}, + "id": "call_1", + "type": "tool_call", + } + ], + ) + ] + } + result = runner.invoke(state) + msg = result["messages"][-1] + assert isinstance(msg, ToolMessage) + assert msg.status == "error" + content = msg.content + assert "SUPER_SECRET_PW" not in content + assert "10.0.0.5" not in content + assert "postgres://" not in content + assert "leaky_tool" in content + assert "RuntimeError" in content + logger.exception.assert_called() + + +def test_tool_runner_success_path_unchanged(): + @tool + def ok_tool(x: str) -> str: + """Return ok.""" + return f"ok:{x}" + + runner = ToolRunner(tools=[ok_tool], logger=MagicMock(spec=Logger)) + state = { + "messages": [ + AIMessage( + content="", + tool_calls=[ + { + "name": "ok_tool", + "args": {"x": "hi"}, + "id": "call_2", + "type": "tool_call", + } + ], + ) + ] + } + result = runner.invoke(state) + msg = result["messages"][-1] + assert isinstance(msg, ToolMessage) + assert msg.status != "error" + assert "ok:hi" in str(msg.content) From a1d3d9a6de07a0f9f7d1cc921f7890d6ff5323f6 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Thu, 6 Aug 2026 20:22:56 -0400 Subject: [PATCH 2/2] ci: retrigger title validation after title fix