From c9761749ecbcd0bb13feef5e8173537c73110974 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 22 Jun 2026 09:05:22 -0700 Subject: [PATCH 1/2] Studio: correct the anyio<4.14 pin rationale (mixed-install ImportError, not a 4.14 cancel-scope bug) (#6579) * Studio: correct the anyio<4.14 pin rationale (mixed-install ImportError) The pin comments said "anyio 4.14+ breaks cancel scope on Python 3.13", but a clean anyio 4.14.0 works on 3.13 (cancel scopes, Event, and the asyncio backend import all pass). The actual failure is a half-resolved install: anyio 4.14 added TaskHandle, imported by __init__.py and _backends/_asyncio from _core/_tasks. When a stale 4.13 _core/_tasks (no TaskHandle) sits under 4.14's importers, the import raises ImportError and 500s the server. Correct the rationale; the <4.14 pin still stands as the way to keep one consistent anyio version. * Clarify the anyio override comment (mixed-install ImportError, not a 4.14 cancel-scope bug) --- studio/backend/requirements/no-torch-runtime.txt | 2 +- studio/backend/requirements/single-env/constraints.txt | 7 +++++-- .../requirements/single-env/overrides-darwin-arm64.txt | 10 ++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/studio/backend/requirements/no-torch-runtime.txt b/studio/backend/requirements/no-torch-runtime.txt index b0157cfea0..a611c009fb 100644 --- a/studio/backend/requirements/no-torch-runtime.txt +++ b/studio/backend/requirements/no-torch-runtime.txt @@ -56,7 +56,7 @@ httpx httpcore certifi idna -anyio>=3.0,<4.14.0 # 4.14+ breaks cancel scope on Py3.13 (#6483) +anyio>=3.0,<4.14.0 # one consistent <4.14: 4.14's TaskHandle importers over a stale 4.13 _core/_tasks -> ImportError (#6483) sniffio h11 diff --git a/studio/backend/requirements/single-env/constraints.txt b/studio/backend/requirements/single-env/constraints.txt index aad4c38664..a916c6fc75 100644 --- a/studio/backend/requirements/single-env/constraints.txt +++ b/studio/backend/requirements/single-env/constraints.txt @@ -13,8 +13,11 @@ fastmcp>=3.0.2 mcp>=1.24,<2 websockets>=15.0.1 -# anyio 4.14+ breaks cancel scope on Python 3.13 (#6483). Global cap so later -# with-deps steps (studio.txt, data-designer-deps.txt) can't re-resolve it up. +# Keep anyio on one consistent <4.14 line. anyio 4.14 added TaskHandle (imported +# by __init__.py and the asyncio backend from _core/_tasks); a clean 4.14 is fine +# on 3.13. The real failure (#6483) is a half-resolved install: a stale 4.13 +# _core/_tasks (no TaskHandle) under 4.14's importers raises ImportError and 500s +# the server. Global cap so later with-deps steps can't re-resolve it up. anyio<4.14.0 pandas==2.3.3 diff --git a/studio/backend/requirements/single-env/overrides-darwin-arm64.txt b/studio/backend/requirements/single-env/overrides-darwin-arm64.txt index 8558cd5b6c..a0e73c7efc 100644 --- a/studio/backend/requirements/single-env/overrides-darwin-arm64.txt +++ b/studio/backend/requirements/single-env/overrides-darwin-arm64.txt @@ -4,8 +4,10 @@ # happens at runtime via the side-car venvs. transformers>=4.57.6 -# mlx-vlm / mlx-lm pull anyio>=4.14, which conflicts with the constraints.txt -# cap (anyio<4.14.0, #6483: 4.14+ breaks cancel scope on Python 3.13). A -c -# constraint loses that conflict on macOS-arm and 4.14.0 gets installed; an -# override wins it, so force anyio down here too. +# mlx-vlm / mlx-lm pull anyio>=4.14, which fights the constraints.txt cap +# (anyio<4.14.0). The -c constraint loses that fight on macOS-arm, leaving a +# half-resolved anyio (4.14 importers over a stale 4.13 _core/_tasks with no +# TaskHandle) that ImportErrors and 500s the server (#6483; clean 4.14 is fine, +# it is the mix that breaks). An override wins the fight, so force one +# consistent <4.14 here too. anyio<4.14.0 From 7ecbf5a770623da25891a3df0be881bf72ee13a2 Mon Sep 17 00:00:00 2001 From: Saicharan Ramineni <84414237+GodlyDonuts@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:06:03 -0400 Subject: [PATCH 2/2] Use UTF-8 for Python code-execution subprocess I/O (#6489 class) (#6548) * Use UTF-8 for Python code-execution subprocess I/O Studio's code-execution tool already tells the child to emit UTF-8 (PYTHONIOENCODING=utf-8 in _build_safe_env), but _python_exec writes the temp script and decodes the subprocess pipe with the OS default codec. On Windows (cp1252), non-ASCII in model-written code or its output -- arrows, CJK, emoji -- raises UnicodeEncodeError / UnicodeDecodeError and breaks execution. Complete the UTF-8 wiring in core/inference/tools.py: - write the temp script with encoding="utf-8" - decode _python_exec stdout as utf-8, errors="replace" - set PYTHONIOENCODING=utf-8 in _build_bypass_env too (matches _build_safe_env, so the bypass path's child also emits utf-8) The child is python with PYTHONIOENCODING=utf-8, so it emits UTF-8 regardless of the console code page and the decode is always correct. Shell execution via cmd.exe has a separate console-code-page story and is left to a follow-up. Refs unslothai/unsloth#6489 * Scope Python exec UTF-8 env to Python tool * Make bash bypass test robust to a host-set PYTHONIOENCODING for PR #6548 Bypass mode preserves benign host env vars, so a host-set PYTHONIOENCODING was inherited into the bash bypass env and tripped the new assertion even though _bash_exec never adds it. Clear it in the test so the assertion checks _bash_exec, not the runner environment. --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han --- studio/backend/core/inference/tools.py | 12 ++++++- .../backend/tests/test_bypass_permissions.py | 6 +++- studio/backend/tests/test_exec_utf8.py | 33 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 studio/backend/tests/test_exec_utf8.py diff --git a/studio/backend/core/inference/tools.py b/studio/backend/core/inference/tools.py index 6960310018..a5c193ff39 100644 --- a/studio/backend/core/inference/tools.py +++ b/studio/backend/core/inference/tools.py @@ -2545,14 +2545,24 @@ def _python_exec( pass try: fd, tmp_path = tempfile.mkstemp(suffix = ".py", prefix = "studio_exec_", dir = workdir) - with os.fdopen(fd, "w") as f: + # utf-8 so non-ASCII in model-written code survives the OS default codec + # (Windows cp1252 would otherwise raise UnicodeEncodeError). + with os.fdopen(fd, "w", encoding = "utf-8") as f: f.write(code) safe_env = _build_bypass_env(workdir) if disable_sandbox else _build_safe_env(workdir) + if disable_sandbox: + # Match the sandboxed Python path without changing bypass shell I/O. + safe_env = dict(safe_env) + safe_env["PYTHONIOENCODING"] = "utf-8" popen_kwargs = dict( stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, + # Decode child output as utf-8 (it emits utf-8 via PYTHONIOENCODING); + # replace so non-ASCII output never crashes the read on Windows. + encoding = "utf-8", + errors = "replace", cwd = workdir, env = safe_env, ) diff --git a/studio/backend/tests/test_bypass_permissions.py b/studio/backend/tests/test_bypass_permissions.py index 563f146816..d92509a5fe 100644 --- a/studio/backend/tests/test_bypass_permissions.py +++ b/studio/backend/tests/test_bypass_permissions.py @@ -135,6 +135,7 @@ def test_python_bypass_uses_bypass_preexec_and_bypass_env(captured_popen, monkey assert captured_popen["kwargs"]["preexec_fn"] is tools._bypass_preexec env = captured_popen["kwargs"]["env"] assert env.get("HOSTVAR") == "benign-xyz" + assert env.get("PYTHONIOENCODING") == "utf-8" assert "HF_TOKEN" not in env @@ -151,9 +152,12 @@ def test_bash_blocklist_skipped_when_bypassed(captured_popen): @_POSIX_ONLY -def test_bash_bypass_uses_bypass_preexec(captured_popen): +def test_bash_bypass_uses_bypass_preexec(captured_popen, monkeypatch): + # bypass inherits benign host vars; clear so we assert _bash_exec adds none. + monkeypatch.delenv("PYTHONIOENCODING", raising = False) _bash_exec("echo hi", None, 5, "t", disable_sandbox = True) assert captured_popen["kwargs"]["preexec_fn"] is tools._bypass_preexec + assert "PYTHONIOENCODING" not in captured_popen["kwargs"]["env"] # ── real end-to-end python execution under bypass ─────────────────── diff --git a/studio/backend/tests/test_exec_utf8.py b/studio/backend/tests/test_exec_utf8.py new file mode 100644 index 0000000000..90b78754ed --- /dev/null +++ b/studio/backend/tests/test_exec_utf8.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +"""_python_exec must round-trip non-ASCII output end to end. + +Model-written code routinely contains non-ASCII (arrows, CJK, emoji). The temp +script and the child's stdout pipe both have to be UTF-8 or it crashes/garbles +on Windows, whose default codec is cp1252. Mirrors the report in +unslothai/unsloth#6489. The child is ``python`` with PYTHONIOENCODING=utf-8, so +it emits UTF-8 on every OS; this proves the round-trip on a UTF-8 host and +guards against a regression to the OS default codec. +""" + +import sys +from pathlib import Path + +import pytest + +_BACKEND_ROOT = Path(__file__).resolve().parents[1] +if str(_BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(_BACKEND_ROOT)) + +from core.inference.tools import _python_exec + +# Arrow, em-dash, accent, CJK, check mark, astral-plane emoji -- none encodable +# in cp1252, so the OS default codec would raise on write or read. +_UNICODE = "café — 数字 → ✓ 😀" + + +@pytest.mark.parametrize("disable_sandbox", [False, True]) +def test_python_exec_round_trips_non_ascii(disable_sandbox): + out = _python_exec(f"print({_UNICODE!r})", disable_sandbox = disable_sandbox) + assert _UNICODE in out, repr(out)