diff --git a/.github/workflows/publish-runtime.yml b/.github/workflows/publish-runtime.yml index 80862e07..c22fbcc7 100644 --- a/.github/workflows/publish-runtime.yml +++ b/.github/workflows/publish-runtime.yml @@ -135,6 +135,14 @@ jobs: WORKSPACE_ID=00000000-0000-0000-0000-000000000000 \ PLATFORM_URL=http://localhost:8080 \ /tmp/smoke/bin/python -c " + # Importing main is the strongest smoke test we can do here: + # main.py is the entry point and pulls every other module + # transitively. If the build script missed an import rewrite + # (e.g. left a bare \`from transcript_auth import ...\` instead + # of \`from molecule_runtime.transcript_auth import ...\` — the + # 0.1.16 incident), this fails with ModuleNotFoundError instead + # of shipping to PyPI and breaking every workspace startup. + import molecule_runtime.main # noqa: F401 from molecule_runtime import a2a_client, a2a_tools from molecule_runtime.builtin_tools import memory from molecule_runtime.adapters import get_adapter, BaseAdapter, AdapterConfig diff --git a/scripts/build_runtime_package.py b/scripts/build_runtime_package.py index 91e121b2..66342bd7 100755 --- a/scripts/build_runtime_package.py +++ b/scripts/build_runtime_package.py @@ -42,8 +42,13 @@ from pathlib import Path # matches one of these) gets rewritten to use the package prefix. # # Closed list (not "every .py we copy") because a typo in workspace/ would -# otherwise leak into a wrong rewrite. Update this when adding a new -# top-level module to workspace/. +# otherwise leak into a wrong rewrite. The set is asserted against +# `workspace/*.py` at build time — if the disk contents drift from this +# list (new module added, old one removed), the build fails loud instead +# of silently shipping unrewritten imports. That gap caused 0.1.16 to +# ship `from transcript_auth import ...` (unrewritten — module added +# without updating this set), which broke every workspace startup with +# `ModuleNotFoundError: No module named 'transcript_auth'`. TOP_LEVEL_MODULES = { "a2a_cli", "a2a_client", @@ -53,15 +58,12 @@ TOP_LEVEL_MODULES = { "adapter_base", "agent", "agents_md", - "claude_sdk_executor", - "cli_executor", "config", "consolidation", "coordinator", "events", "executor_helpers", "heartbeat", - "hermes_executor", "initial_prompt", "main", "molecule_ai_status", @@ -69,7 +71,10 @@ TOP_LEVEL_MODULES = { "plugins", "preflight", "prompt", + "runtime_wedge", "shared_runtime", + "transcript_auth", + "watcher", } # Subdirectory packages — these are already real packages (they have or will @@ -250,6 +255,26 @@ def main() -> int: print(f"error: source not a directory: {src}", file=sys.stderr) return 2 + # Drift gate: assert TOP_LEVEL_MODULES matches workspace/*.py. + # Without this, a new top-level module added to workspace/ ships + # with unrewritten `from import` statements that explode at + # runtime with ModuleNotFoundError. (See 0.1.16 transcript_auth + # incident — closed list silently went stale.) + on_disk_modules = { + f.stem for f in src.glob("*.py") + if f.stem not in {"__init__", "conftest"} + } + missing = on_disk_modules - TOP_LEVEL_MODULES + stale = TOP_LEVEL_MODULES - on_disk_modules + if missing or stale: + print("error: TOP_LEVEL_MODULES drifted from workspace/*.py contents:", file=sys.stderr) + if missing: + print(f" in workspace/ but NOT in TOP_LEVEL_MODULES (will ship un-rewritten): {sorted(missing)}", file=sys.stderr) + if stale: + print(f" in TOP_LEVEL_MODULES but NOT in workspace/ (no-op, but misleading): {sorted(stale)}", file=sys.stderr) + print(" Edit scripts/build_runtime_package.py:TOP_LEVEL_MODULES to match.", file=sys.stderr) + return 3 + pkg_dir = out / "molecule_runtime" print(f"[build] source: {src}") print(f"[build] output: {out}")