ADR-013 Phase 2: enforcement + signed shelf + adopt (adversarial-review hardened)

This commit is contained in:
2026-07-18 21:06:38 -04:00
parent 0ff5edc553
commit 77ca2e0d2d
14 changed files with 1234 additions and 22 deletions

View File

@@ -45,6 +45,8 @@ class PluginLoader:
self._loaded_plugins: Dict[str, BasePlugin] = {}
self._plugin_classes: Dict[str, Type[BasePlugin]] = {}
self._manifests: Dict[str, dict] = {}
# Set by PluginManager.init_app; a PluginVerifier or None (no policy).
self.verifier = None
def _is_strict_mode(self, app: Optional[Flask]) -> bool:
"""Return True if loader should re-raise instead of isolating failures."""
@@ -150,19 +152,38 @@ class PluginLoader:
if name in self._plugin_classes:
return self._plugin_classes[name]
plugin_module_path = self.plugins_dir / name / 'plugin.py'
plugin_dir = self.plugins_dir / name
plugin_module_path = plugin_dir / 'plugin.py'
if not plugin_module_path.exists():
raise PluginNotFoundError(
f'Plugin {name} plugin.py not found at {plugin_module_path}',
plugin_name=name,
)
# THE import gate. Every import path reaches here, so an unsigned or
# tampered plugin never executes under enforcement (findings #1, #2).
# When enforcing: strip any bytecode (so only verified source can run),
# verify the tree, and import without writing new bytecode.
import contextlib
from .packaging import strip_bytecode, no_bytecode
enforcing = self.verifier is not None and self.verifier.require_signed
if enforcing:
strip_bytecode(plugin_dir)
if self.verifier is not None:
ok, reason = self.verifier.check(name)
if not ok:
raise PluginContractError(
f'Plugin {name} failed signature verification: {reason}',
plugin_name=name,
)
try:
spec = importlib.util.spec_from_file_location(
f'plugins.{name}.plugin', plugin_module_path,
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with (no_bytecode() if enforcing else contextlib.nullcontext()):
spec = importlib.util.spec_from_file_location(
f'plugins.{name}.plugin', plugin_module_path,
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except Exception as e:
raise PluginContractError(
f'Plugin {name} import failed: {e}',
@@ -198,6 +219,10 @@ class PluginLoader:
manifest = self.load_manifest(name)
self.check_contract_version(name, __contract_version__)
# verify-at-load is enforced inside load_plugin_class (the single
# import choke point), so EVERY path that imports plugin code -
# load_plugin, discover_available, CLI introspection - fails closed.
for dep in manifest.get('dependencies', []):
if not self.registry.is_enabled(dep):
raise PluginDependencyError(