From 9c280712c316439cebacc37a4dc15e5704381eeb Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:53:46 +0200 Subject: [PATCH 1/2] fix(bundler): reject non-string manifest list members Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/bundler/models/manifest.py | 4 +++- tests/contract/test_manifest_schema.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/bundler/models/manifest.py b/src/specify_cli/bundler/models/manifest.py index 032863a2e8..d4fc735b8a 100644 --- a/src/specify_cli/bundler/models/manifest.py +++ b/src/specify_cli/bundler/models/manifest.py @@ -247,7 +247,9 @@ def _parse_str_list(raw: Any, field_name: str) -> tuple[str, ...]: return () if isinstance(raw, (str, bytes)) or not isinstance(raw, (list, tuple)): raise BundlerError(f"'{field_name}' must be a list of strings when present.") - return tuple(str(item) for item in raw) + if any(not isinstance(item, str) for item in raw): + raise BundlerError(f"'{field_name}' must be a list of strings when present.") + return tuple(raw) def _parse_refs(kind: str, raw: Any) -> list[ComponentRef]: diff --git a/tests/contract/test_manifest_schema.py b/tests/contract/test_manifest_schema.py index 2f38620423..4784bdf462 100644 --- a/tests/contract/test_manifest_schema.py +++ b/tests/contract/test_manifest_schema.py @@ -165,6 +165,25 @@ def test_string_mcp_rejected_not_split_per_character(): BundleManifest.from_dict(data) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("tags", [1]), + ("requires.tools", [False]), + ("requires.mcp", [{}]), + ], +) +def test_string_list_fields_reject_non_string_members(field, value): + data = valid_manifest_dict() + if field == "tags": + data["tags"] = value + else: + data["requires"][field.split(".", 1)[1]] = value + + with pytest.raises(BundlerError, match="must be a list of strings"): + BundleManifest.from_dict(data) + + def test_string_integration_rejected_not_silently_dropped(): # A present-but-non-mapping 'integration' (a bare string) was silently # dropped, leaving the bundle wrongly integration-agnostic. Reject it like From 02fc077827ce6319ce6bb2922d1ba9209d59e6c2 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:13:17 +0200 Subject: [PATCH 2/2] docs(bundler): clarify string list validation Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/bundler/models/manifest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/bundler/models/manifest.py b/src/specify_cli/bundler/models/manifest.py index d4fc735b8a..39684b2327 100644 --- a/src/specify_cli/bundler/models/manifest.py +++ b/src/specify_cli/bundler/models/manifest.py @@ -237,11 +237,11 @@ def _text(raw: Any) -> str: def _parse_str_list(raw: Any, field_name: str) -> tuple[str, ...]: - """Coerce a manifest list-of-strings field into a tuple of strings. + """Parse a manifest list-of-strings field into a tuple of strings. Rejects a bare string/bytes (which would otherwise be iterated - character-by-character) and any non-list/tuple, matching the manifest - contract (``string[]``). + character-by-character), any non-list/tuple, and any non-string member, + matching the manifest contract (``string[]``). """ if raw is None: return ()