exlab_wizard.plugins.registry#

Plugin registry – manifest-driven discovery + dispatch resolution.

Backend Spec §6.2.

The registry scans two roots (bundled + lab) for plugin directories, parses each manifest.yml without importing any plugin Python code, validates the manifest against the spec’s schema (§6.1.2), enforces the api_version gate (§6.2.1), enforces the network-opt-in gate (§6.3.3), and exposes a dispatch surface (candidates_for) that matches files to plugins by extension.

Lab plugins win on name collision with bundled plugins (§6.2.1.4); the collision is logged so operators can audit which copy is in effect.

This module is host-only: it does not import the plugin’s Plugin class – that import is deferred to the worker subprocess to preserve the §6.3 crash-isolation guarantee. PluginRecord.plugin_class is therefore typed as type | None and is only populated by the --no-isolation test path; production code reads the plugin via the manifest + source path and lets the worker do the import.

Classes

PluginManifest(name, version, author, ...[, ...])

Parsed manifest.yml contents.

PluginPlan(record, matching_files)

One plugin paired with the files in the rendered tree it matches.

PluginRecord(manifest, plugin_class, ...)

One plugin in the registry.

PluginRegistry(bundled_dir, lab_dir[, ...])

Manifest-driven plugin registry.

RegistryReport([loaded, rejected])

Outcome of a PluginRegistry.reload() pass.

class exlab_wizard.plugins.registry.PluginManifest(name, version, author, description, supported_extensions, api_version, required_variables=<factory>, optional_variables=<factory>, isolation=<factory>)[source]#

Bases: object

Parsed manifest.yml contents. Backend Spec §6.1.2.

Mirrors the schema documented in the spec, with default values for optional blocks. isolation is a normalized dict containing timeout_seconds, memory_mb, and network – the raw YAML is rejected if any of these exceed their hard caps.

Parameters:
api_version: str#
author: str#
description: str#
isolation: dict[str, Any]#
name: str#
optional_variables: list[str]#
required_variables: list[str]#
supported_extensions: list[str]#
version: str#
class exlab_wizard.plugins.registry.PluginPlan(record, matching_files)[source]#

Bases: object

One plugin paired with the files in the rendered tree it matches.

Built by PluginRegistry.candidates_for() for each plugin whose supported_extensions matched at least one file.

Parameters:
matching_files: list[Path]#
record: PluginRecord#
class exlab_wizard.plugins.registry.PluginRecord(manifest, plugin_class, source_path, source_root)[source]#

Bases: object

One plugin in the registry.

Carries the parsed manifest, the source-on-disk plugin directory, and the source root identifier (BUNDLED or LAB) so the Settings UI can show where each plugin came from.

plugin_class is None in the production path (the host doesn’t import plugin code); it’s only populated when a caller explicitly requests in-process testing via PluginRegistry --no-isolation helpers (Backend Spec §6.10). Backend Spec §6.2.1.

Parameters:
manifest: PluginManifest#
plugin_class: type | None#
source_path: Path#
source_root: PluginSourceRoot#
class exlab_wizard.plugins.registry.PluginRegistry(bundled_dir, lab_dir, supported_api_versions=frozenset({'1'}), allow_network=False)[source]#

Bases: object

Manifest-driven plugin registry.

Constructed at host startup with the two configured plugin roots; one call to reload() populates the in-memory record table by walking each root, parsing each manifest.yml, and applying the validation gates from §6.1.2 / §6.2.1.

Lab plugins win on name collision with bundled plugins (§6.2.1.4); a structured INFO log entry records the override.

Parameters:
candidates_for(file_paths)[source]#

Resolve plugins that match the given files by extension.

For each registered plugin, collects the subset of file_paths whose suffix matches one of the plugin’s supported_extensions. Plugins with at least one matching file appear in the returned list; the order is alphabetical by plugin name (the host applies template-declared ordering on top – see §6.2.3).

Parameters:

file_paths (list[Path])

Return type:

list[PluginPlan]

get(name)[source]#

Return the record registered under name, or None.

Parameters:

name (str)

Return type:

PluginRecord | None

list_all()[source]#

Return all registered records, sorted by name for stable output.

Return type:

list[PluginRecord]

reload()[source]#

Re-scan both plugin roots and rebuild the in-memory record table.

Bundled plugins are scanned first; lab plugins are scanned second and replace any same-named bundled record. Plugins that fail validation (missing manifest, malformed YAML, bad api_version, excessive isolation limits, network-opt-in declined) are excluded from the table and added to the report’s rejected list with a short reason string.

Backend Spec §6.2.1.

Return type:

RegistryReport

class exlab_wizard.plugins.registry.RegistryReport(loaded=<factory>, rejected=<factory>)[source]#

Bases: object

Outcome of a PluginRegistry.reload() pass.

loaded is the list of plugin names that survived validation; rejected is a list of (name, reason) tuples for plugins that were skipped, with the reason short enough for a single log line. Used by the Settings UI to surface a load summary.

Parameters:
loaded: list[str]#
rejected: list[tuple[str, str]]#