From e95170911edd21730539a94f74473fb8ebe17029 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 30 Oct 2026 02:07:33 -0400 Subject: [PATCH] Prepare declared distribution requirements in the target runtime --- src/agent_comms/installer/probes/packages.py | 11 ++++++ .../installer/runtime_resolution.py | 14 +++++++- src/agent_comms/installer/upgrade.py | 3 ++ src/agent_comms/runtime_preparation.py | 35 +++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/agent_comms/runtime_preparation.py diff --git a/src/agent_comms/installer/probes/packages.py b/src/agent_comms/installer/probes/packages.py index 8b9eec0dca..37f16c7c36 100644 --- a/src/agent_comms/installer/probes/packages.py +++ b/src/agent_comms/installer/probes/packages.py @@ -16,6 +16,17 @@ from agent_comms.field_codec import WireValue from agent_comms.installer.interpreter_operation import RuntimeProbe, TargetProbe from agent_comms.package_stamp import STAMP +from agent_comms.runtime_preparation import RuntimePreparation + + +@dataclass(frozen=True) +class PreparePackageProbe(TargetProbe, RuntimeProbe[None]): + """Only the built target loads its distribution's preparation implementation.""" + + distribution: str + + def run(self) -> None: + RuntimePreparation.load(self.distribution).prepare() @dataclass(frozen=True) diff --git a/src/agent_comms/installer/runtime_resolution.py b/src/agent_comms/installer/runtime_resolution.py index bf6271ad73..9149fc23c5 100644 --- a/src/agent_comms/installer/runtime_resolution.py +++ b/src/agent_comms/installer/runtime_resolution.py @@ -15,7 +15,10 @@ from agent_comms.declared_family import DeclaredFamily from agent_comms.installer.probes.packages import ArchivePin, GitPin, PackagePin, PackageRecord -from agent_comms.installer.probes.packages import DistributionVersionsProbe, InstalledPackagesProbe +from agent_comms.installer.interpreter_operation import RuntimeProbe +from agent_comms.installer.probes.packages import ( + DistributionVersionsProbe, InstalledPackagesProbe, PreparePackageProbe, +) from agent_comms.launch_runtime import LaunchRuntime from agent_comms.repository_name import RepositoryName @@ -31,6 +34,11 @@ class RuntimePackage(DeclaredFamily, affix="Package"): def commit(cls, resolution: RuntimeResolution) -> str: return resolution.packages[cls.distribution].source.revision() + @classmethod + def preparations(cls) -> tuple[RuntimeProbe[None], ...]: + """Only packages declaring requirements add target-runtime operations.""" + return () + class ConsoleScriptPackage: """A runtime package whose console scripts the installer publishes.""" @@ -49,6 +57,10 @@ class ToadPackage(RuntimePackage, ConsoleScriptPackage): distribution = "batrachian-toad" repository = RepositoryName("OpenHCSDev", "toad") + @classmethod + def preparations(cls) -> tuple[RuntimeProbe[None], ...]: + return (PreparePackageProbe(cls.distribution),) + class TextualPackage(RuntimePackage): distribution = "textual" diff --git a/src/agent_comms/installer/upgrade.py b/src/agent_comms/installer/upgrade.py index 975f6d3968..38ec480a8a 100644 --- a/src/agent_comms/installer/upgrade.py +++ b/src/agent_comms/installer/upgrade.py @@ -705,6 +705,9 @@ def install_native(self) -> None: def prepare_runtime(self) -> None: self.probe(PrepareProducerDigestsProbe()) self.probe(PrepareNativeProbe()) + for package in RuntimePackage.members_with(RuntimePackage): + for preparation in package.preparations(): + self.probe(preparation) def publish(self, root: Path) -> None: """The selected interpreter owns console metadata and harness declarations, and diff --git a/src/agent_comms/runtime_preparation.py b/src/agent_comms/runtime_preparation.py new file mode 100644 index 0000000000..af3fc56b99 --- /dev/null +++ b/src/agent_comms/runtime_preparation.py @@ -0,0 +1,35 @@ +"""The nominal preparation contract loaded from a target distribution's metadata.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from importlib.metadata import distribution +from inspect import isabstract +from typing import ClassVar + + +class RuntimePreparation(ABC): + """A distribution prepares its own requirements before runtime publication.""" + + entry_point_group: ClassVar[str] = "agent_comms.runtime_preparation" + + @abstractmethod + def prepare(self) -> None: ... + + @classmethod + def decode(cls, value: object) -> RuntimePreparation: + """The entry point is foreign until its concrete nominal class is checked.""" + if not isinstance(value, type): + raise TypeError("Runtime preparation entry point must load a class") + if not issubclass(value, cls) or isabstract(value): + raise TypeError(f"Runtime preparation must implement {cls.__qualname__}") + return value() + + @classmethod + def load(cls, package: str) -> RuntimePreparation: + entries = tuple(distribution(package).entry_points.select(group=cls.entry_point_group)) + if len(entries) != 1: + raise ValueError( + f"{package} declares {len(entries)} {cls.entry_point_group} entry points; expected one" + ) + return cls.decode(entries[0].load())