Skip to content

Entry-point extensions

base_cli.ExtensionDiscovery provides an optional, lazy discovery boundary for framework extensions. The core package only reads package metadata when a consumer creates a discovery instance; third-party code is imported only when an extension is explicitly loaded.

The supported entry-point groups are:

Group Contract Purpose
base_cli.commands A callable command registrar Add commands to a consumer-owned App or Click tree
base_cli.profiles A callable profile factory Supply a CliProfile for a named consumer
base_cli.plugins A callable plugin installer Register a coordinated extension with a consumer

For example, a package can publish:

[project.entry-points."base_cli.commands"]
audit = "acme_cli.audit:register"

[project.entry-points."base_cli.profiles"]
acme = "acme_cli.profile:build_profile"

[project.entry-points."base_cli.plugins"]
telemetry = "acme_cli.telemetry:install"

The loaded callable receives the arguments documented by the consuming application. base-cli intentionally discovers metadata without imposing a single command-tree or profile-construction shape; this keeps Click, Typer, and consumer-owned composition boundaries independent.

Typed SDK and compatibility

The supported callable protocols are exported as CommandExtension, ProfileExtension, and PluginExtension. They are intentionally small: consumers own the concrete App, CliProfile, and service composition types, while plugin packages type-check against a stable callback boundary.

Declare the SDK version and capabilities in the entry-point extras. The base-cli-api-v1 extra selects the current protocol version; additional base-cli-cap-<name> extras advertise optional behavior without importing the plugin:

[project.entry-points."base_cli.plugins"]
telemetry = "acme_cli.telemetry:install [base-cli-api-v1, base-cli-cap-tracing]"

ExtensionDescriptor.api_version and .capabilities expose this metadata. ExtensionDiscovery rejects an unsupported API version before loading the entry point and reports the supported versions in the error. Capabilities are currently informational inventory metadata: they do not grant or restrict permissions and do not sandbox an in-process Python extension. A consumer that needs a capability policy must inspect the descriptor and make that decision before calling load().

This is a negotiation boundary, not an installation-order heuristic; the consumer can select another plugin release or widen its supported_api_versions policy after running its compatibility suite.

An entry point must declare at most one base-cli-api-vN extra. Discovery skips an entry point with malformed metadata on a per-entry-point basis, so a bad third-party package cannot prevent healthy commands, profiles, or plugins from being listed and loaded.

Determinism and safety

Descriptors are ordered by group, entry-point name, distribution, version, and target value. Duplicate names are an error; installation order is never an implicit precedence rule. Each descriptor retains distribution/version/extras metadata for diagnostics and policy decisions.

Discovery is cached per ExtensionDiscovery instance. Call refresh() after a runtime environment change. load_all() isolates broken third-party imports and returns an ExtensionLoadResult for every descriptor so one broken plugin does not hide healthy extensions.

Use allowlist={"base_cli.commands:audit"} to restrict one entry point, or allowlist={"dist:acme-cli"} to trust all entry points from one distribution. Distribution allowlist values use the exact installed metadata spelling, including case; base-cli does not silently normalize case, hyphens, or underscores for a security decision. ExtensionDiscovery(disabled=True) disables discovery entirely. Bare entry point names and unqualified distribution names are rejected because they can silently trust an unintended group or distribution.

Loading uses the exact entry-point source and distribution identity approved by discovery rather than re-selecting an entry point by a potentially colliding name/value tuple. If that identity changes before loading, the load fails closed. An allowlist is a selection boundary, not a sandbox for installed Python code.