Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ and versions are tracked in the repo-root `VERSION` file.

- Honor combined positive/negative JSON option declarations and explicit
`--no-json` values when deciding whether pre-parse errors use JSON output.
- Isolate malformed third-party entry-point metadata so one invalid extension
cannot prevent healthy extensions from being discovered.

### Added

Expand Down
5 changes: 5 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ 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
Expand Down
7 changes: 6 additions & 1 deletion lib/python/base_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,12 @@ def _metadata_descriptors(self) -> tuple[ExtensionDescriptor, ...]:
value = getattr(entry_point, "value", None)
if group not in ENTRY_POINT_GROUPS or not isinstance(name, str) or not isinstance(value, str):
continue
descriptor = _descriptor_from_entry_point(entry_point)
try:
descriptor = _descriptor_from_entry_point(entry_point)
except ValueError:
# Malformed metadata belongs to one third-party distribution;
# do not let it hide healthy extensions from discovery.
continue
if self._allowed(descriptor):
descriptors.append(descriptor)
descriptors.sort(key=_descriptor_sort_key)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ def test_allowlist_and_disable_switch_are_enforced(self) -> None:
with self.assertRaises(base_cli.ExtensionsDisabledError):
disabled.load(base_cli.COMMAND_ENTRY_POINT_GROUP, "allowed")

def test_malformed_metadata_is_skipped_without_hiding_healthy_extensions(self) -> None:
malformed = _entry_point(
"broken",
"broken:register",
extras=("base-cli-api-v1", "base-cli-api-v2"),
)
healthy_command = _entry_point("healthy", "healthy:register")
healthy_profile = _entry_point(
"profile",
"healthy:profile",
group=base_cli.PROFILE_ENTRY_POINT_GROUP,
)
discovery = base_cli.ExtensionDiscovery(
entry_points=(malformed, healthy_command, healthy_profile),
)

self.assertEqual([item.name for item in discovery.list_commands()], ["healthy"])
self.assertEqual([item.name for item in discovery.list_profiles()], ["profile"])
self.assertEqual(
[result.descriptor.name for result in discovery.load_all(base_cli.COMMAND_ENTRY_POINT_GROUP)],
["healthy"],
)

def test_load_all_isolates_broken_extensions(self) -> None:
healthy = _entry_point("healthy", "one:register")
broken = _entry_point("broken", "two:register")
Expand Down
Loading