From ebb51b2cef877326bb4ce37e4b8086f628429945 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sat, 27 Jun 2026 19:58:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20(client):=20Redact=20resolve?= =?UTF-8?q?d=20secrets=20in=20DispatchToolResult=20repr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DispatchToolResult.resolved_args holds post-substitution resolved credential values. The dataclass-generated __repr__ rendered them verbatim, leaking secrets into logs, tracebacks, and debugger output (CWE-532). Add a redacting __repr__/__str__ that shows only the arg key names and count, mirroring GatewayClient.__repr__ (AAASM-3642). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019mSz31RysZF6DYToUoBWLf --- agent_assembly/client/dispatch.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/agent_assembly/client/dispatch.py b/agent_assembly/client/dispatch.py index e2387978..8ce4458d 100644 --- a/agent_assembly/client/dispatch.py +++ b/agent_assembly/client/dispatch.py @@ -6,7 +6,10 @@ from typing import Any -@dataclass(frozen=True) +# ``repr=False``: ``resolved_args`` holds resolved secret values, so the +# dataclass-generated ``__repr__`` (which renders every field verbatim) would +# leak them into logs/tracebacks. We supply a redacting ``__repr__`` instead. +@dataclass(frozen=True, repr=False) class DispatchToolResult: """ Outcome of a successful ``dispatch_tool`` call. @@ -17,7 +20,9 @@ class DispatchToolResult: Attributes: resolved_args: Post-substitution args. Carries the *resolved* - credential values; do not log this or pass it to the LLM. + credential values; do not log this or pass it to the LLM. The + ``repr``/``str`` of this object deliberately masks these values + (key names and count only) so they cannot leak via logging. names_substituted: The placeholder names that were resolved during this call. Names only — never the resolved values. Echoes the audit-log shape so callers can correlate dispatches with audit @@ -26,3 +31,19 @@ class DispatchToolResult: resolved_args: dict[str, Any] names_substituted: list[str] = field(default_factory=list) + + def __repr__(self) -> str: + """Render without exposing resolved secret values. + + Shows the argument *keys* and their count in place of the resolved + values, so the result can be safely logged or appear in tracebacks. + ``names_substituted`` is names-only by contract and is shown verbatim. + """ + keys = sorted(self.resolved_args) + return ( + "DispatchToolResult(" + f"resolved_args=, " + f"names_substituted={self.names_substituted!r})" + ) + + __str__ = __repr__ From 801cbfa0eb95d050450b4e71987d282958923b61 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sat, 27 Jun 2026 20:00:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20(client):=20Assert=20DispatchTo?= =?UTF-8?q?olResult=20repr=20masks=20resolved=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression test for CWE-532: constructs a result holding a secret value and asserts neither repr() nor str() leaks it, while still exposing arg key names and count for debuggability. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019mSz31RysZF6DYToUoBWLf --- test/unit/client/test_dispatch_tool.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/client/test_dispatch_tool.py b/test/unit/client/test_dispatch_tool.py index 204b3d0e..1705999d 100644 --- a/test/unit/client/test_dispatch_tool.py +++ b/test/unit/client/test_dispatch_tool.py @@ -97,3 +97,21 @@ async def test_dispatch_tool_defaults_empty_result_fields_when_server_omits_them assert result.resolved_args == {} assert result.names_substituted == [] + + +def test_dispatch_tool_result_repr_does_not_leak_resolved_secret_values() -> None: + """repr/str must mask resolved_args values, exposing only keys (CWE-532).""" + secret = "sk-live-super-secret-token-9f8e7d" + result = DispatchToolResult( + resolved_args={"api_key": secret, "endpoint": "https://api.example.com"}, + names_substituted=["api_key"], + ) + + for rendered in (repr(result), str(result)): + assert secret not in rendered + # Key names and a count are shown so the object stays debuggable. + assert "api_key" in rendered + assert "endpoint" in rendered + assert "2 value(s)" in rendered + # names_substituted is names-only by contract; safe to render verbatim. + assert "names_substituted=['api_key']" in rendered