mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(extensions): guard the required manifest sections so one bad extension cannot break extension list (#3797)
ExtensionManifest.REQUIRED_FIELDS only checks key PRESENCE, so a section that is
written but left empty (`provides:` -> None) or given the wrong shape
(`provides: []`) passes it and then fails on first use:
extension: null -> TypeError: argument of type 'NoneType' is not iterable
requires: null -> TypeError: argument of type 'NoneType' is not iterable
provides: null -> AttributeError: 'NoneType' object has no attribute 'get'
provides: [] -> AttributeError: 'list' object has no attribute 'get'
Neither is a ValidationError, so both escape the callers that already handle
malformed manifests. list_installed() catches ValidationError only and has a
deliberate "Corrupted extension" fallback, so a single bad extension took down
the whole command -- reproduced end-to-end:
before: specify extension list -> exit 1, raw AttributeError, no output
after: specify extension list -> exit 0, the good extension listed, the
bad one shown as "Corrupted extension"
Add an isinstance guard for each required section, mirroring the nested guards
already in this function ("Invalid provides.commands: expected a list", "Invalid
hooks: expected a mapping") and _load_yaml's document-root check. Only the three
REQUIRED sections lacked one.
`provides: {}` is unaffected: it is a well-shaped mapping, so an extension that
provides only hooks still validates, and with no hooks it keeps the pre-existing
"must provide at least one command or hook" message. Both are locked by tests.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -263,8 +263,25 @@ class ExtensionManifest:
|
||||
f"(expected {self.SCHEMA_VERSION})"
|
||||
)
|
||||
|
||||
# The REQUIRED_FIELDS loop above only checks key PRESENCE, so a section
|
||||
# that is written but left empty (``provides:`` -> None) or given the
|
||||
# wrong shape (``provides: []``) passes it and then fails on first use:
|
||||
# ``field not in None`` raises TypeError and ``None.get(...)`` raises
|
||||
# AttributeError. Neither is a ValidationError, so both escape the
|
||||
# callers that already handle malformed manifests -- list_installed()'s
|
||||
# "Corrupted extension" fallback catches ValidationError only, so one bad
|
||||
# extension made ``specify extension list`` exit 1 with a raw
|
||||
# AttributeError instead of listing the rest. Guard each required
|
||||
# section's shape, mirroring the nested guards below ("Invalid
|
||||
# provides.commands: expected a list", "Invalid hooks: expected a
|
||||
# mapping") and _load_yaml's document-root check.
|
||||
|
||||
# Validate extension metadata
|
||||
ext = self.data["extension"]
|
||||
if not isinstance(ext, dict):
|
||||
raise ValidationError(
|
||||
f"Invalid extension: expected a mapping, got {type(ext).__name__}"
|
||||
)
|
||||
for field in ["id", "name", "version", "description"]:
|
||||
if field not in ext:
|
||||
raise ValidationError(f"Missing extension.{field}")
|
||||
@@ -299,11 +316,19 @@ class ExtensionManifest:
|
||||
|
||||
# Validate requires section
|
||||
requires = self.data["requires"]
|
||||
if not isinstance(requires, dict):
|
||||
raise ValidationError(
|
||||
f"Invalid requires: expected a mapping, got {type(requires).__name__}"
|
||||
)
|
||||
if "speckit_version" not in requires:
|
||||
raise ValidationError("Missing requires.speckit_version")
|
||||
|
||||
# Validate provides section
|
||||
provides = self.data["provides"]
|
||||
if not isinstance(provides, dict):
|
||||
raise ValidationError(
|
||||
f"Invalid provides: expected a mapping, got {type(provides).__name__}"
|
||||
)
|
||||
commands = provides.get("commands", [])
|
||||
hooks = self.data.get("hooks")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user