Skip to content

Extension contracts and discovery

flayer.extensions is an additive Python API for explicit provider and deployment profile registration. The existing lifecycle core, CLI, configuration, ownership, state, and artifact contracts remain unchanged. ADR 0011 records the discovery and loading boundary.

Implemented contracts

Contract Responsibility
ExtensionDescriptor Immutable kind, identifier, distribution, exact version, group, and factory.
ProviderExtension Construct the existing read-only CloudProvider from ProviderContext.
LifecycleProviderExtension Optionally construct the separate existing LifecycleProvider.
ProfileExtension Compile a profile's strict configuration with exact ProfileContext ownership.
CompiledProfile Expose identity, server bundle generation, and verified lifecycle TOML rendering.
ExtensionRegistry Register, select, and explicitly load exact validated descriptors.

ProviderContext contains only scope_id, an external CLI authentication_reference, and bounded command timeout/inventory limits. It does not resolve credentials, read credential files, accept credential bytes, select executables, or carry an arbitrary options dictionary. An authentication reference is an identifier naming an already configured external authentication source; resolved tokens and key material must never be supplied as identifiers. ProfileContext carries the existing validated StackIdentity. Configuration values are validated by the selected profile schema, which rejects unknown fields and keeps private credentials out of generated server configuration.

The extension object exposes its exact Descriptor. External entry points name a zero-argument factory returning that object. The object must satisfy its group's protocol and return the exact discovered descriptor, including its selected metadata snapshot. A factory can obtain its descriptor by statically reading its own explicitly located .dist-info directory through DiscoverExtensions. Structural protocol validation checks required callable methods and identity; the trusted implementation remains responsible for method semantics and outputs.

Usable built-ins

Kind Identifier Adapter
Provider yandex-read-only YandexCloudProvider; inventory and exact resource lookup.
Provider yandex-lifecycle Separate read-only factory and YandexLifecycleProvider factory.
Profile secure-gateway Real strict gateway parser, compiler, server bundle, and plan.

All built-ins use the package's exact version. Listing descriptors and constructing their adapters perform no provider operations. The read-only provider extension does not implement the optional lifecycle factory. A caller must deliberately select that separate factory before using existing mutation methods.

"""Select a real scoped read-only adapter without performing cloud operations."""

from flayer.extensions import (
    ExtensionKind,
    ExtensionRegistry,
    ProviderContext,
    ProviderExtension,
)

registry = ExtensionRegistry()
descriptor = registry.Get(ExtensionKind.PROVIDER, "yandex-read-only")
extension = registry.Load(descriptor)
assert isinstance(extension, ProviderExtension)
provider = extension.CreateProvider(ProviderContext("example-folder", "external-profile"))

For a gateway, select ExtensionKind.PROFILE and secure-gateway, then call CompileProfile(configuration, ProfileContext(identity)). The compiled adapter rejects any configuration ownership mismatch. BuildServerBundle() produces the existing owned artifact bundle in memory. Store it with WriteArtifactBundle() before calling RenderPlan(user_data_file=...); rendering verifies the exact owned server bytes and manifest. It does not deploy the plan or prove guest or transport readiness.

Static metadata discovery

DiscoverExtensions(metadata_directories) accepts an explicit bounded sequence of absolute, non-redirected .dist-info directories. It reads only METADATA, entry_points.txt, and RECORD under each selected directory. It does not traverse Python search paths, enumerate installed packages, invoke metadata finder hooks, scan source modules, follow final symlinks, import plugin packages, query networks, install packages, or inspect infrastructure. Directory and file counts, file sizes, names, targets, and versions are bounded.

Supported entry-point groups are flayer.providers.v1 and flayer.profiles.v1. For example, an installed distribution's entry_points.txt can declare:

[flayer.providers.v1]
example = example_plugin.plugin:CreateExtension

Other ecosystems' groups are ignored. Unknown F-Layer groups, unsupported ABI versions, malformed targets, extras, duplicate metadata fields, and duplicate extension identities fail closed. Names are bounded lowercase identifiers. Distribution names are normalized to lowercase hyphens, and only stable three-component release versions are supported. External distributions cannot claim f-layer or target the reserved flayer module namespace. Registration rejects any duplicate (kind, plugin_id), including built-in shadowing.

Descriptors contain a SHA-256 snapshot of the three metadata files. They provide inspectable declared identity; they do not verify publisher identity, signatures, installation provenance, or the authenticity of the package.

Explicit loading and source ownership

Register discovered descriptors with ExtensionRegistry(descriptors). Discovery does not load them. Load(descriptor, allowed=(exact_descriptor,)) is the separate explicit trust decision for external code. The descriptor must match an exact registry entry and an exact caller allowlist entry; the pin includes distribution, version, group, target, selected location, and metadata digest. Application policy must decide which inspected descriptors are trusted before populating the allowlist. Built-ins are restricted to the exact known registry descriptors.

Before import, loading rechecks the metadata snapshot and verifies SHA-256/size ownership in RECORD for the target module and every parent package. Each source must be an exact regular non-linked file inside the selected installation root. Source-only Python modules and ordinary packages are supported; namespace packages, binary extensions, missing parent ownership, ambiguous package/module targets, weak or missing hashes, and changed files fail closed.

Loading executes the verified immutable source bytes directly. A temporary import hook serves only the already selected source chain so a parent's import of its target uses those same bytes rather than stale bytecode or ambient source resolution. The hook is removed after loading, including failures. Preloaded same-name modules are rejected even when they report the correct file path, unless this loader holds a receipt binding the exact module object, source path, and executed source hash. Exact loader-owned source may be reused; changed code in an already loaded module chain requires a fresh process. Registry repetition returns the already validated extension without re-executing its factory.

External dependency imports and factory execution occur only after opt-in and are part of trusting that package and environment. Loading is not a code sandbox, dependency validator, or protection against an actively hostile process. A failure removes only modules and receipts created by this loader attempt; it cannot undo arbitrary plugin side effects or dependency imports. Runtime failures are sanitized, and loaded descriptor/protocol mismatches fail closed.

Validation evidence

Deterministic tests cover discovery without finder or module execution, malformed metadata, ABI versions, duplicates and built-in shadowing, exact loading consent, metadata/source drift, RECORD ownership, stale cached modules and bytecode, sanitized failures, typed context bounds, real Yandex adapter construction, and a complete local secure-gateway artifact-to-plan workflow. No tests use real cloud operations or credentials.

Repository source