Skip to content

ADR 0011: Static extension discovery and explicit loading

Status

Accepted for Task #29.

Context

The read-only Yandex adapter, its separate lifecycle adapter, and secure-gateway profile are real extension consumers. Their contracts already separate resource observation, owned mutation, and profile compilation. Requiring a generic mutation interface from every provider would break this boundary.

Installed entry points are executable import targets. Discovering available extensions must not import their modules, execute factories, query infrastructure, or invoke installed metadata finder hooks. Searching arbitrary directories or network locations would also create an implicit trust and scope expansion.

Decision

Introduce flayer.extensions with immutable descriptors and an explicit registry. Built-in descriptors name usable adapters for the existing implementations. External discovery reads bounded METADATA, entry_points.txt, and RECORD only from the exact .dist-info directories selected by the caller. It does not enumerate installed distributions or search paths. Versioned groups are flayer.providers.v1 and flayer.profiles.v1. Plugin identifiers, distribution identities, exact stable release versions, groups, and import targets are validated before a descriptor enters the registry. Duplicates and unsupported F-Layer groups fail closed.

External loading is a separate explicit operation. It requires the exact registered descriptor and an exact descriptor allowlist, including its import target. Metadata files are pinned by digest and rechecked before import so a changed discovery snapshot cannot authorize loading. Exact RECORD hashes and sizes bind the selected target and every parent package to regular source files under the selected installation root. The reserved flayer namespace is excluded from external targets. Loading executes verified in-memory source snapshots; a temporary hook serves only selected-chain imports so parent imports cannot execute stale bytecode. Cached modules require loader-owned object/path/source-hash receipts. Loading checks the factory's extension protocol and exact declared identity. It is an explicit code execution trust decision, not a sandbox or a package authenticity check.

Provider factories return the existing CloudProvider protocol. A separate LifecycleProviderExtension returns the existing LifecycleProvider protocol. Profiles compile explicit configuration with an exact StackIdentity context and return an adapter exposing server bundle generation and lifecycle plan rendering. Bounded provider context contains a scope, an external authentication reference, and execution limits; context never carries resolved credential bytes. Profile configuration remains subject to the profile's own strict schema.

Consequences

Importing the extension package and discovering metadata perform no plugin execution. An operator can inspect a catalog before authorizing an exact external factory. The existing CLI and lifecycle core remain unchanged. Constructing a provider or compiling a built-in profile does not mutate infrastructure.

Only stable three-component release versions, source-only ordinary Python packages, and entry points without extras are supported initially. Adding another ABI requires a new group and explicit compatibility decision. Operators supply selected metadata directories; automatic installation, package resolution, credential management, dynamic module scanning, and process isolation are outside this contract. Pins do not verify signatures, publisher authenticity, or dependency ownership. The selected environment and external package must be trusted before loading. External dependencies remain part of the opted-in execution trust boundary. Cleanup cannot undo arbitrary plugin side effects; only this attempt's created modules and receipts are removed.

Alternatives

Automatic importlib.metadata finder traversal can invoke installed finder code and expands discovery scope. Importing modules to inspect capabilities executes unapproved code. A generic plugin framework, dependency resolver, or lifecycle replacement adds speculative contracts without improving the current consumers.

Compatibility and migration

Existing provider, lifecycle, profile, state, artifact, and CLI contracts remain unchanged. Built-in extension adapters are additive. Existing consumers can keep using the direct APIs; no configuration or persisted-state migration is required.

References

Repository source