From a225c1b3732473dfe84bef7562e93ca1146b59bd Mon Sep 17 00:00:00 2001 From: theater <1347507191@qq.com> Date: Mon, 14 Sep 2026 23:11:33 +0800 Subject: [PATCH] fix(cli): don't crash the run TUI when streamed output contains a literal [...] (#7435) * fix(cli): reject non-serializable literal_eval results in TUI JSON formatting _try_parse_structured() accepted any dict/list coming out of ast.literal_eval(), including values json.dumps() cannot encode such as [Ellipsis] from a literal [...]. _format_json_in_text() then raised TypeError: Object of type ellipsis is not JSON serializable, which propagated through _tick and cancelled the whole crew run. Validate the parsed object with json.dumps() inside _try_parse_structured() so only JSON-serializable dict/list values are returned; anything else falls back to the original text. Fixes #7434. * fix(cli): contain RecursionError in the TUI JSON formatting boundary A streamed structure nested deeper than the JSON backend can walk could raise RecursionError out of _try_parse_structured (from json.loads) or out of the render-path dumps, escaping _tick and losing the TUI update. Catch RecursionError when loading, and validate literal_eval results with the exact kwargs the render path uses, so any structure the render cannot encode is rejected at the boundary and the raw text renders instead. --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- lib/cli/src/crewai_cli/crew_run_tui.py | 8 +++++- lib/cli/tests/test_crew_run_tui.py | 38 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index 75390f2a2..d1ff1a37f 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -86,13 +86,19 @@ def _try_parse_structured(text: str) -> Any | None: """Try JSON first, then Python repr (single-quoted dicts/lists).""" try: return _json.loads(text) - except (ValueError, TypeError): + except (ValueError, TypeError, RecursionError): pass try: import ast obj = ast.literal_eval(text) + # literal_eval accepts values json.dumps cannot encode (e.g. [Ellipsis] + # from "[...]"), which would crash the renderer later; reject those. + # Validate with the exact kwargs the render path uses so a structure + # the C decoder accepts but the indent encoder cannot walk is also + # rejected here instead of raising inside _tick. if isinstance(obj, (dict, list)): + _json.dumps(obj, indent=2, ensure_ascii=False) return obj except Exception: # noqa: S110 pass diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 6cca73eee..23c16dc6e 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -41,6 +41,8 @@ from crewai_cli.crew_run_tui import ( _LOG_ARGS_TEXT_LIMIT, _LOG_RESULT_TEXT_LIMIT, _LOG_TRUNCATION_SUFFIX, + _format_json_in_text, + _try_parse_structured, ) @@ -1804,3 +1806,39 @@ def test_finished_traces_button_still_records(monkeypatch) -> None: app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces-done"))) app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces") + + +def test_try_parse_structured_rejects_non_serializable_literals() -> None: + """ast.literal_eval("[...]") is a valid [Ellipsis] list but cannot be JSON-encoded.""" + assert _try_parse_structured("[...]") is None + assert _try_parse_structured("{'a': ...}") is None + assert _try_parse_structured("[1+2j]") is None + + +def test_try_parse_structured_still_accepts_serializable_values() -> None: + assert _try_parse_structured('{"a": 1}') == {"a": 1} + assert _try_parse_structured("['x', 'y']") == ["x", "y"] + assert _try_parse_structured("{'a': 1}") == {"a": 1} + + +def test_format_json_in_text_survives_literal_ellipsis() -> None: + """A streamed [...] must render as-is instead of crashing the TUI (issue #7434).""" + assert _format_json_in_text("pandas,[...]") == "pandas,[...]" + + +def test_try_parse_structured_rejects_deeply_nested_input() -> None: + """A nesting depth no JSON backend can walk must parse as nothing, not recurse.""" + deep = "[" * 100_000 + "]" * 100_000 + assert _try_parse_structured(deep) is None + + +def test_format_json_in_text_survives_deep_nesting() -> None: + """The render path must contain the failure instead of losing the TUI update.""" + deep = "data: " + "[" * 4000 + "]" * 4000 + assert isinstance(_format_json_in_text(deep), str) + + +def test_format_json_in_text_still_pretty_prints_valid_json() -> None: + assert _format_json_in_text('data: {"a": 1} and [...]') == ( + 'data: ' + '{\n "a": 1\n}' + ' and [...]' + )