Skip to content

Commit eb60cff

Browse files
committed
Flatten tool-result whitespace in the -v flow log
Untrusted tool output (a fetched page, a search payload) is logged in the verbose flow log; embedded CR/LF could forge fake `[aai_cli.…]` log lines. `_clip` now collapses all whitespace to single spaces before truncating, so each result stays on one line and can't inject log lines. (Secrets remain masked separately by the debuglog formatter.) Addresses the Aikido review note on PR #243. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YQmYYiZDEZkDvWqXYk6D
1 parent a8c09a0 commit eb60cff

2 files changed

Lines changed: 22 additions & 4 deletions

File tree

aai_cli/agent_cascade/brain.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,18 @@ def _log_flow(state: dict[str, object], seen: int) -> int:
219219

220220

221221
def _clip(text: str) -> str:
222-
"""Truncate a tool result for the flow log, marking that it was shortened."""
223-
if len(text) <= _RESULT_LOG_CAP:
224-
return text
225-
return f"{text[:_RESULT_LOG_CAP]}… ({len(text)} chars)"
222+
"""Flatten a tool result onto one line and truncate it for the flow log.
223+
224+
Tool output is untrusted external content (a fetched page, a search payload), so its
225+
whitespace — newlines especially — is collapsed before logging: a result can't then
226+
forge extra ``[aai_cli.…]`` log lines, and each result stays on one readable line. The
227+
length is capped so a multi-KB payload can't bury the rest of the flow. (Secrets are
228+
separately masked by the debuglog formatter across every record.)
229+
"""
230+
flattened = " ".join(text.split())
231+
if len(flattened) <= _RESULT_LOG_CAP:
232+
return flattened
233+
return f"{flattened[:_RESULT_LOG_CAP]}… ({len(flattened)} chars)"
226234

227235

228236
def _reply_text(result: dict[str, object]) -> str:

tests/test_agent_cascade_brain.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,16 @@ def test_clip_passes_short_text_and_truncates_long_text():
248248
assert len(clipped) < len(long)
249249

250250

251+
def test_clip_flattens_whitespace_so_tool_output_cant_forge_log_lines():
252+
# Tool output is untrusted: a result with embedded CR/LF could otherwise inject fake
253+
# "[aai_cli.…]" log lines. _clip collapses all whitespace runs to single spaces, so the
254+
# result stays on one line.
255+
forged = "ok\n[aai_cli.agent_cascade.brain] tool call rm_rf args={}\r\nmore"
256+
assert brain._clip(forged) == "ok [aai_cli.agent_cascade.brain] tool call rm_rf args={} more"
257+
assert "\n" not in brain._clip(forged)
258+
assert "\r" not in brain._clip(forged)
259+
260+
251261
# --- _reply_text / _content_text ---------------------------------------------
252262

253263

0 commit comments

Comments
 (0)