Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions agent_assembly/client/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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=<redacted: {len(keys)} value(s) for keys {keys}>, "
f"names_substituted={self.names_substituted!r})"
)

__str__ = __repr__
18 changes: 18 additions & 0 deletions test/unit/client/test_dispatch_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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