Issue #5 (Python core migration, CAS state transitions, shadow watcher, auto-completion gate, eager-import immutable runtime, B-13 freeze removal) is complete and merged to main. This stamps the release. SemVer: MINOR, not MAJOR. The change is large but breaks nothing. Across the whole Issue #5 range there are zero file deletions and zero renames, the event wire schema stays at SCHEMA_VERSION 1, all four literal script paths survive as compatibility shims, the 968-line legacy shell loop is still in run_loop.sh, and every default change reverts through a single environment variable (MAM_DELEGATION_PY / MAM_ENGINE_CAS / MAM_LOOP_PY / MAM_LOOP_FREEZE). The riskiest new feature, auto-completion, ships default-off. No operator has anything to migrate, which is the decisive reason not to call it 5.0.0. The reasoning is recorded in the VERSIONS.md release notes, not just here. Close a gap in the lockstep guard. lib.sh exports MAM_VERSION and eager_load() refuses to start the loop when it disagrees with FRAMEWORK_VERSION, so eager.py is a load-bearing fourth leg — but test_version_consistency.py only checked three, and the sole check on eager.py was a hardcoded literal in the phase 4 test that a release bump has to edit anyway. The lockstep test now covers eager.py, a new test pins the fail-closed gate itself, and the phase 4 test no longer carries a version literal. Fix a latent test-isolation defect the bump exposed. The first full run failed four tests with "MAM_VERSION mismatch: expected 4.4.0, got 4.3.0 in environment" — 4.3.0 was not in the tree, it was inherited from the shell that launched pytest and had sourced the pre-bump lib.sh. Those tests were measuring the runner's shell rather than the tree under test, and only passed until now because the two values always happened to agree. A session-scoped fixture drops the ambient value. Production is unaffected: run_loop.sh:17 sources lib.sh before anything else and lib.sh re-exports MAM_VERSION unconditionally, so a stale inherited value is always overwritten before Python starts. Full suite: 750 passed in 912.50s, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
125 lines
3.4 KiB
Python
125 lines
3.4 KiB
Python
"""Eager module loading for immutable runtime immunity (Issue #5 Phase 4).
|
|
|
|
Ensures all dependencies (loop, delegation, agents, external packages)
|
|
are loaded into sys.modules at loop startup, preventing mid-flight disk
|
|
modifications from triggering lazy-import syntax/EOF errors (B-13).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
import sys
|
|
from typing import List, Sequence
|
|
|
|
# Framework version locked with framework constants
|
|
FRAMEWORK_VERSION = "4.4.0"
|
|
|
|
CORE_MODULES: Sequence[str] = (
|
|
# Standard library modules
|
|
"atexit",
|
|
"argparse",
|
|
"copy",
|
|
"glob",
|
|
"hashlib",
|
|
"json",
|
|
"os",
|
|
"pathlib",
|
|
"re",
|
|
"shutil",
|
|
"signal",
|
|
"sqlite3",
|
|
"subprocess",
|
|
"sys",
|
|
"tempfile",
|
|
"time",
|
|
"typing",
|
|
# Framework loop modules
|
|
"lib_py.loop.parser",
|
|
"lib_py.loop.checkpoint",
|
|
"lib_py.loop.runner",
|
|
"lib_py.loop.attribution",
|
|
"lib_py.loop.autocomplete",
|
|
"lib_py.loop.evidence",
|
|
"lib_py.loop.policy",
|
|
"lib_py.loop.shadow",
|
|
"lib_py.loop.store",
|
|
"lib_py.loop.watcher",
|
|
# Delegation and common modules
|
|
"lib_py.paths",
|
|
"lib_py.state",
|
|
"lib_py.atomic_yaml",
|
|
"lib_py.workspace_uuid",
|
|
"lib_py.verify_session",
|
|
"lib_py.layout",
|
|
"lib_py.delegation.engine",
|
|
"lib_py.delegation.finalize",
|
|
"lib_py.delegation.job_subscriber",
|
|
"lib_py.delegation.mqtt_common",
|
|
"lib_py.delegation.notifier",
|
|
"lib_py.delegation.publish_event",
|
|
"lib_py.delegation.publisher",
|
|
"lib_py.delegation.registry",
|
|
"lib_py.delegation.service",
|
|
"lib_py.delegation.cli",
|
|
# Agent adapter registry
|
|
"lib_py.agents.base",
|
|
"lib_py.agents.registry",
|
|
"lib_py.agents.input_region",
|
|
"lib_py.agents.sanitize",
|
|
)
|
|
|
|
OPTIONAL_MODULES: Sequence[str] = (
|
|
"yaml",
|
|
"paho.mqtt",
|
|
"paho.mqtt.client",
|
|
)
|
|
|
|
|
|
def verify_framework_version(expected_version: str = FRAMEWORK_VERSION) -> None:
|
|
"""Verifies that MAM_VERSION matches framework version if set."""
|
|
env_version = os.environ.get("MAM_VERSION")
|
|
if env_version and env_version != expected_version:
|
|
raise RuntimeError(
|
|
f"MAM_VERSION mismatch: expected {expected_version}, got {env_version} in environment."
|
|
)
|
|
|
|
|
|
def eager_load(
|
|
modules: Sequence[str] = CORE_MODULES,
|
|
optional_modules: Sequence[str] = OPTIONAL_MODULES,
|
|
) -> List[str]:
|
|
"""Eagerly imports all modules into sys.modules.
|
|
|
|
Returns the list of successfully loaded module names.
|
|
"""
|
|
verify_framework_version()
|
|
|
|
# Ensure PYTHONDONTWRITEBYTECODE is set for isolation
|
|
sys.dont_write_bytecode = True
|
|
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
|
|
loaded: List[str] = []
|
|
for mod_name in modules:
|
|
if mod_name not in sys.modules:
|
|
try:
|
|
importlib.import_module(mod_name)
|
|
except Exception as exc:
|
|
raise RuntimeError(f"Failed to eager load {mod_name}: {exc}") from exc
|
|
loaded.append(mod_name)
|
|
|
|
for opt_mod in optional_modules:
|
|
if opt_mod not in sys.modules:
|
|
try:
|
|
importlib.import_module(opt_mod)
|
|
loaded.append(opt_mod)
|
|
except (ImportError, ModuleNotFoundError):
|
|
pass
|
|
|
|
return loaded
|
|
|
|
|
|
def is_eager_loaded(modules: Sequence[str] = CORE_MODULES) -> bool:
|
|
"""Returns True if all required core modules are present in sys.modules."""
|
|
return all(mod in sys.modules for mod in modules)
|