From a6d77fc9102ce280313d5e05435862b3be6dacb9 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:10:41 +0530 Subject: [PATCH] Isolate malformed extension metadata --- CHANGELOG.md | 5 +++++ docs/extensions.md | 5 +++++ lib/python/base_cli/extensions.py | 7 ++++++- tests/test_extensions.py | 23 +++++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f216c..9e57917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ and versions are tracked in the repo-root `VERSION` file. values, and header-style `key: value` arguments before they reach logs or persisted history. +### Fixed + +- Isolate malformed third-party entry-point metadata so one invalid extension + cannot prevent healthy extensions from being discovered. + ### Added - Add a framework choice guide, five-minute evaluation path, and clearer diff --git a/docs/extensions.md b/docs/extensions.md index 120c652..46f120a 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -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 diff --git a/lib/python/base_cli/extensions.py b/lib/python/base_cli/extensions.py index 602e69b..9ed36a1 100644 --- a/lib/python/base_cli/extensions.py +++ b/lib/python/base_cli/extensions.py @@ -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) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index f40f713..19a4b85 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -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")