From 75c0e5475fff1b874dcbc4a52fe0e9c20e90fba2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 15:02:48 +0900 Subject: [PATCH] fix(strix): resolve strix.interface.main package-attribute shadow strix/interface/__init__.py in strix-agent 1.5.3 runs `from .main import main`, which rebinds the package attribute `strix.interface.main` to the `main` function it imports, shadowing the submodule of the same name. `from strix.interface import main as strix_main` therefore returned the function, not the module, and `strix_main.asyncio = ...` raised `AttributeError: 'function' object has no attribute 'asyncio'` on every single invocation of the launcher this repo installs for every Strix run (CWL_STRIX_UNBOUNDED_INFERENCE=1 is set unconditionally by the installer). Confirmed live: every sampled completed Strix run across the org crashes in ~2 seconds, before any LLM call, with this exact traceback (e.g. DiagramWeave run 33598375361, job 100146441461) -- and the gate script fails the required check closed on any non-zero exit, so this has been failing Strix's required check on every PR org-wide since the launcher merged (f59bad1, 2026-09-01). Fix: look the submodule up directly in sys.modules by its exact dotted path, which the shadow never touches (verified against a real strix-agent 1.5.3 install). The existing test suite's synthetic strix.interface fake never replicated the shadow (it set the package attribute directly to the module), so it never caught the crash the real package produces -- updated both fakes to replicate the real __init__.py's shadowing behavior and added a dedicated regression test that reproduces it. Co-Authored-By: Claude Sonnet 5 --- scripts/ci/strix_timeout_compat.py | 14 +++++- tests/test_strix_llm_timeout_contract.py | 56 +++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/scripts/ci/strix_timeout_compat.py b/scripts/ci/strix_timeout_compat.py index 25eef5b277..7ddb290654 100755 --- a/scripts/ci/strix_timeout_compat.py +++ b/scripts/ci/strix_timeout_compat.py @@ -12,6 +12,7 @@ import importlib.metadata import os +import sys from collections.abc import Awaitable, MutableMapping from functools import wraps from typing import Any @@ -83,7 +84,18 @@ def make_model_settings_without_request_deadline(*args: Any, **kwargs: Any) -> A scan_setup.asyncio = UnboundedInferenceAsyncio(scan_setup.asyncio) - from strix.interface import main as strix_main + # strix/interface/__init__.py runs ``from .main import main``, which rebinds + # the package attribute ``strix.interface.main`` to the *function* it + # imports, shadowing the submodule of the same name. Both + # ``from strix.interface import main as strix_main`` and + # ``import strix.interface.main as strix_main`` resolve through that + # shadowed package attribute and return the function, not the module, so + # every ``strix_main.`` access below raised AttributeError. Look the + # submodule up directly in sys.modules by its exact dotted path instead, + # which the shadow never touches. + import strix.interface.main # noqa: F401 - imported for its sys.modules registration + + strix_main = sys.modules["strix.interface.main"] strix_main.asyncio = UnboundedInferenceAsyncio(strix_main.asyncio) return strix_main diff --git a/tests/test_strix_llm_timeout_contract.py b/tests/test_strix_llm_timeout_contract.py index 8661486441..b46630c898 100644 --- a/tests/test_strix_llm_timeout_contract.py +++ b/tests/test_strix_llm_timeout_contract.py @@ -127,7 +127,12 @@ def make_model_settings(*args, **kwargs): main_module.main = lambda: None core_package.inputs = inputs_module interface_package.scan_setup = scan_setup_module - interface_package.main = main_module + # Real strix/interface/__init__.py runs ``from .main import main``, which + # rebinds the package attribute to the *function*, shadowing the + # submodule of the same name. Replicate that shadow here so this test + # actually exercises the sys.modules lookup path instead of the + # attribute-traversal path a shadow-unaware fake would take. + interface_package.main = main_module.main strix_package.core = core_package strix_package.interface = interface_package @@ -373,7 +378,9 @@ def test_launcher_script_entrypoint_enters_patched_strix(monkeypatch) -> None: main_module.main = lambda: calls.append("main") core_package.inputs = inputs_module interface_package.scan_setup = scan_setup_module - interface_package.main = main_module + # Replicate strix/interface/__init__.py's ``from .main import main`` shadow + # (see the sibling test above) so this also exercises the real code path. + interface_package.main = main_module.main strix_package.core = core_package strix_package.interface = interface_package monkeypatch.setitem(sys.modules, "strix", strix_package) @@ -393,3 +400,48 @@ def test_launcher_script_entrypoint_enters_patched_strix(monkeypatch) -> None: runpy.run_path(str(LAUNCHER), run_name="__main__") assert calls == ["main"] + + +def test_runtime_compatibility_survives_the_package_level_main_shadow(monkeypatch) -> None: + """Regression: strix/interface/__init__.py's ``from .main import main`` shadows the + submodule as a package attribute, so attribute-traversal imports of + ``strix.interface.main`` return the function, not the module — this reproduces the + live crash (AttributeError: 'function' object has no attribute 'asyncio') seen in + production before the sys.modules lookup fix.""" + launcher = _load_launcher() + + strix_package = types.ModuleType("strix") + core_package = types.ModuleType("strix.core") + interface_package = types.ModuleType("strix.interface") + inputs_module = types.ModuleType("strix.core.inputs") + scan_setup_module = types.ModuleType("strix.interface.scan_setup") + main_module = types.ModuleType("strix.interface.main") + + inputs_module.make_model_settings = lambda *args, **kwargs: kwargs + scan_setup_module.asyncio = asyncio + main_module.asyncio = asyncio + main_module.main = lambda: None + core_package.inputs = inputs_module + interface_package.scan_setup = scan_setup_module + # The shadow itself: the package attribute is the bare function, exactly as + # ``from .main import main`` leaves it in the real strix-agent 1.5.3 package. + interface_package.main = main_module.main + strix_package.core = core_package + strix_package.interface = interface_package + + monkeypatch.setitem(sys.modules, "strix", strix_package) + monkeypatch.setitem(sys.modules, "strix.core", core_package) + monkeypatch.setitem(sys.modules, "strix.core.inputs", inputs_module) + monkeypatch.setitem(sys.modules, "strix.interface", interface_package) + monkeypatch.setitem(sys.modules, "strix.interface.scan_setup", scan_setup_module) + monkeypatch.setitem(sys.modules, "strix.interface.main", main_module) + monkeypatch.setattr(launcher, "_require_supported_version", lambda: None) + monkeypatch.setenv("LLM_TIMEOUT", "300") + monkeypatch.setenv("LLM_STREAM_IDLE_TIMEOUT", "300") + + assert isinstance(interface_package.main, types.FunctionType) + + result = launcher.install_runtime_compatibility() + + assert result is main_module + assert isinstance(main_module.asyncio, launcher.UnboundedInferenceAsyncio)