Skip to content

Commit 03cf307

Browse files
anticomputerCopilot
andcommitted
Log tool-call environment variable names instead of values
Debug logging in mcp_client_params printed the full tool-call environment, which includes resolved credentials such as GH_TOKEN. Log only the variable names so secrets are never written to logs, while keeping the names for debuggability. The environment passed to the MCP server is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8de19ce commit 03cf307

2 files changed

Lines changed: 52 additions & 4 deletions

File tree

src/seclab_taskflow_agent/mcp_utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ async def call_tool(self, *args: Any, **kwargs: Any) -> Any:
172172
ClientParamsMap = dict[str, tuple[dict[str, Any], list[str], str | None, int | None]]
173173

174174

175+
def _env_names(env: dict[str, Any] | None) -> list[str] | None:
176+
"""Return only the environment variable names, for safe debug logging.
177+
178+
Tool-call environments routinely carry credentials (e.g. ``GH_TOKEN``), so
179+
their values must never be written to logs. Callers log the names to keep
180+
debug output useful without leaking secrets.
181+
"""
182+
return sorted(env) if env else env
183+
184+
175185
def mcp_client_params(
176186
available_tools: AvailableTools,
177187
requested_toolboxes: list[str],
@@ -202,7 +212,7 @@ def mcp_client_params(
202212
case "stdio":
203213
env = dict(sp.env) if sp.env else None
204214
args = list(sp.args) if sp.args else None
205-
logging.debug("Initializing toolbox: %s\nargs:\n%s\nenv:\n%s\n", tb, args, env)
215+
logging.debug("Initializing toolbox: %s\nargs:\n%s\nenv names:\n%s\n", tb, args, _env_names(env))
206216
if env:
207217
for k, v in list(env.items()):
208218
try:
@@ -217,7 +227,7 @@ def mcp_client_params(
217227
"http_proxy", "https_proxy", "no_proxy"):
218228
if proxy_var not in env and proxy_var in os.environ:
219229
env[proxy_var] = os.environ[proxy_var]
220-
logging.debug("Tool call environment: %s", env)
230+
logging.debug("Tool call environment names: %s", _env_names(env))
221231
if args:
222232
for i, v in enumerate(args):
223233
args[i] = swap_env(v)
@@ -241,7 +251,7 @@ def mcp_client_params(
241251
if sp.command is not None:
242252
env = dict(sp.env) if sp.env else None
243253
args = list(sp.args) if sp.args else None
244-
logging.debug("Initializing streamable toolbox: %s\nargs:\n%s\nenv:\n%s\n", tb, args, env)
254+
logging.debug("Initializing streamable toolbox: %s\nargs:\n%s\nenv names:\n%s\n", tb, args, _env_names(env))
245255
exe = shutil.which(sp.command)
246256
if exe is None:
247257
raise FileNotFoundError(f"Could not resolve path to {sp.command}")

tests/test_mcp_utils.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from __future__ import annotations
77

88
import asyncio
9+
import logging
910
from types import SimpleNamespace
1011
from unittest.mock import AsyncMock, MagicMock
1112

1213
import pytest
1314

14-
from seclab_taskflow_agent.mcp_utils import MCPNamespaceWrap, compress_name
15+
from seclab_taskflow_agent.mcp_utils import MCPNamespaceWrap, compress_name, mcp_client_params
1516

1617

1718
class _FakeTool:
@@ -145,3 +146,40 @@ def test_list_tools_existing_behaviour_unchanged():
145146
obj.list_tools.assert_awaited_once_with(run_context="ctx", agent="agent")
146147
ns = compress_name("RepoContext")
147148
assert result[0].name == f"{ns}read_file"
149+
150+
151+
# -- secret redaction in debug logs --
152+
153+
154+
def test_mcp_client_params_logs_env_names_not_secret_values(monkeypatch, caplog):
155+
# A tool-call environment routinely resolves credentials like GH_TOKEN;
156+
# debug logging must record only the variable names, never the values.
157+
sentinel = "do-not-log-this-value"
158+
monkeypatch.setenv("GH_TOKEN", sentinel)
159+
server_params = SimpleNamespace(
160+
kind="stdio",
161+
reconnecting=False,
162+
env={"GH_TOKEN": "{{ env('GH_TOKEN') }}", "LOG_DIR": "/tmp/logs"},
163+
args=None,
164+
command="echo",
165+
)
166+
toolbox = SimpleNamespace(
167+
server_params=server_params,
168+
confirm=[],
169+
server_prompt=None,
170+
client_session_timeout=None,
171+
)
172+
available_tools = MagicMock()
173+
available_tools.get_toolbox.return_value = toolbox
174+
175+
with caplog.at_level(logging.DEBUG):
176+
params = mcp_client_params(available_tools, ["pkg.tb"])
177+
178+
# The resolved secret value never reaches the logs ...
179+
assert sentinel not in caplog.text
180+
# ... but the variable names are still logged for debuggability.
181+
assert "GH_TOKEN" in caplog.text
182+
assert "LOG_DIR" in caplog.text
183+
# Redaction is log-only: the real env (with the resolved secret) is still
184+
# passed through to the MCP server.
185+
assert params["pkg.tb"][0]["env"]["GH_TOKEN"] == sentinel

0 commit comments

Comments
 (0)