From c814aa221007608232c406b3a4d73c8bc4ae103d Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-Runtime-BE Date: Sun, 10 May 2026 12:02:56 +0000 Subject: [PATCH] fix(workspace): register plugins_registry as sys.modules shim before loading adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KI-296 fix: when the PyPI-installed runtime (molecule-ai-workspace-runtime 0.1.129+) ships plugins_registry as molecule_runtime.plugins_registry (a subpackage), plugin adapter files that do ``from plugins_registry import ...`` as a top-level name fail with ModuleNotFoundError because Python's import system cannot find a top-level ``plugins_registry`` package. The fix in plugins_registry/__init__.py:_load_module_from_path() registers molecule_runtime.plugins_registry as ``plugins_registry`` in sys.modules before exec'ing any plugin adapter file, so the top-level import resolves correctly in both environments: - PyPI wheel (molecule_runtime.plugins_registry → sys.modules["plugins_registry"]) - molecule-core workspace source (top-level workspace/plugins_registry already on sys.path; the setdefault is a no-op) Submodules (builtins, protocol, raw_drop) are also registered so adapters that import ``from plugins_registry.builtins import ...`` work without error. Added test_load_module_from_path_registers_plugins_registry_sys_modules. Co-Authored-By: Claude Opus 4.7 --- workspace/plugins_registry/__init__.py | 26 +++++++++++++++ workspace/tests/test_plugins_registry.py | 41 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/workspace/plugins_registry/__init__.py b/workspace/plugins_registry/__init__.py index 363f26fe..18e517ad 100644 --- a/workspace/plugins_registry/__init__.py +++ b/workspace/plugins_registry/__init__.py @@ -51,6 +51,32 @@ class AdaptorSource: def _load_module_from_path(module_name: str, path: Path): """Import a Python file by absolute path. Returns the module or None on failure.""" + + # KI-296: Before exec'ing plugin-adapter files (which import + # ``from plugins_registry import ...`` as a top-level name), register + # the molecule-runtime subpackage as ``plugins_registry`` in sys.modules. + # In the molecule-core workspace source this is already a top-level package, + # so the setdefault is a no-op. In the PyPI-installed runtime wheel + # (molecule-ai-workspace-runtime 0.1.129+), the package ships as + # ``molecule_runtime.plugins_registry`` and without this shim every + # plugin adapter would fail with ModuleNotFoundError. + import sys as _sys + + if "plugins_registry" not in _sys.modules: + try: + _mr_pr = __import__("molecule_runtime.plugins_registry", fromlist=[""]) + _sys.modules["plugins_registry"] = _mr_pr + # Also register submodules the adapters commonly import directly. + for _sub in ("builtins", "protocol", "raw_drop"): + _submod = getattr(_mr_pr, _sub, None) + if _submod is not None: + _sys.modules[f"plugins_registry.{_sub}"] = _submod + except ImportError: + # molecule-runtime not installed (e.g. test environment with + # workspace/ on sys.path directly) — skip shim; the top-level + # workspace/plugins_registry package is already findable. + pass + spec = importlib.util.spec_from_file_location(module_name, path) if spec is None or spec.loader is None: return None diff --git a/workspace/tests/test_plugins_registry.py b/workspace/tests/test_plugins_registry.py index 44531eb4..ebfb7e1a 100644 --- a/workspace/tests/test_plugins_registry.py +++ b/workspace/tests/test_plugins_registry.py @@ -325,3 +325,44 @@ def test_resolve_registry_missing_module_falls_through(monkeypatch, tmp_path: Pa monkeypatch.setattr(pr, "_REGISTRY_ROOT", tmp_path / "empty-registry") _, source = pr.resolve("demo-plugin", "test_runtime", plugin_root) assert source == AdaptorSource.RAW_DROP + + +def test_load_module_from_path_registers_plugins_registry_sys_modules(tmp_path: Path): + """KI-296: _load_module_from_path registers ``plugins_registry`` in sys.modules + before exec'ing the adapter, so adapter files that do + ``from plugins_registry import ...`` resolve correctly when the runtime is + installed from the PyPI wheel (where the package ships as + ``molecule_runtime.plugins_registry`` rather than a top-level ``plugins_registry``). + """ + import sys as _sys + import plugins_registry as pr + + # Create a fake adapter that imports plugins_registry at top level. + adapter_file = tmp_path / "fake_runtime_adapter.py" + adapter_file.write_text( + "from plugins_registry import InstallContext # noqa: F401\n" + "from plugins_registry.builtins import AgentskillsAdaptor as Adaptor # noqa: F401\n" + ) + + # Evict any pre-existing sys.modules entries for the shim keys so the + # import inside _load_module_from_path actually runs. + _saved = { + k: _sys.modules.pop(k, None) + for k in ( + "plugins_registry", "plugins_registry.builtins", + "plugins_registry.protocol", "plugins_registry.raw_drop", + "_plugin_adaptor.test.fake_runtime", + ) + } + + try: + result = pr._load_module_from_path("_plugin_adaptor.test.fake_runtime", adapter_file) + assert result is not None, "module should load without ImportError" + assert hasattr(result, "Adaptor"), "AgentskillsAdaptor alias should be in namespace" + finally: + # Restore sys.modules state. + for k, v in _saved.items(): + if v is None: + _sys.modules.pop(k, None) + else: + _sys.modules[k] = v