fix(integrations): escape multiline/control chars in SKILL.md frontmatter#3392
fix(integrations): escape multiline/control chars in SKILL.md frontmatter#3392Quratulain-bilal wants to merge 2 commits into
Conversation
…tter the hand-built SKILL.md frontmatter in the skills setup path (base.py, and the hermes twin) wrapped values in a double-quoted yaml scalar but only escaped backslash and quote. a multiline (block-scalar) description was then emitted with raw newlines inside the quoted scalar and reparsed with those newlines collapsed to spaces; a control character would break yaml loading outright. the description comes from the template frontmatter, so this is reachable for any command with a block-scalar description. add a shared specify_cli/_yaml_string.quote_yaml_double helper that emits a valid double-quoted scalar (newline/cr/tab short escapes, other control chars as \xXX) and route both renderers through it. added a regression test that round-trips a multiline description through the yaml parser.
There was a problem hiding this comment.
Pull request overview
Fixes YAML frontmatter generation for skills-based integrations by centralizing proper YAML double-quoted scalar escaping, preventing multiline descriptions and control characters from corrupting or breaking generated SKILL.md files.
Changes:
- Added
specify_cli/_yaml_string.quote_yaml_double()to escape newlines/CR/tab and other control characters into valid YAML escapes. - Switched the manual
SKILL.mdfrontmatter renderers inSkillsIntegrationandHermesIntegrationto use the shared quoting helper. - Added a regression test to ensure multiline template descriptions round-trip through YAML parsing.
Show a summary per file
| File | Description |
|---|---|
| tests/integrations/test_integration_base_skills.py | Adds regression coverage for multiline description round-trip in generated SKILL.md frontmatter. |
| src/specify_cli/integrations/hermes/init.py | Uses shared YAML quoting helper for Hermes skill frontmatter rendering. |
| src/specify_cli/integrations/base.py | Uses shared YAML quoting helper for skills frontmatter rendering in the base skills setup path. |
| src/specify_cli/_yaml_string.py | Introduces a shared, reusable YAML double-quoted scalar escaping helper. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Low
| def test_skill_frontmatter_preserves_multiline_description( | ||
| self, tmp_path, monkeypatch | ||
| ): | ||
| """A multiline (block-scalar) description must round-trip exactly. | ||
|
|
||
| The hand-built SKILL.md frontmatter used to only escape backslash and | ||
| quote, so a block-scalar description was emitted with raw newlines inside | ||
| a double-quoted scalar and reparsed with those newlines collapsed to | ||
| spaces. The description must survive byte-for-byte.""" | ||
| i = get_integration(self.KEY) | ||
| template = tmp_path / "sample.md" | ||
| template.write_text( | ||
| "---\n" | ||
| "description: |\n" | ||
| " first line\n" | ||
| " second line\n" | ||
| "scripts:\n" | ||
| " sh: scripts/bash/x.sh\n" | ||
| "---\n" | ||
| "Body\n", | ||
| encoding="utf-8", | ||
| ) | ||
| monkeypatch.setattr(i, "list_command_templates", lambda: [template]) | ||
|
|
||
| m = IntegrationManifest(self.KEY, tmp_path) | ||
| created = i.setup(tmp_path, m) | ||
| skill_files = [f for f in created if f.name == "SKILL.md"] | ||
| assert len(skill_files) == 1 | ||
|
|
||
| content = skill_files[0].read_text(encoding="utf-8") | ||
| fm = yaml.safe_load(content.split("---", 2)[1]) | ||
| assert "\n" in fm["description"] | ||
| assert fm["description"] == "first line\nsecond line\n" |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback and resolve conflicts
the existing test only exercised the multiline half of the fix. add a round-trip that feeds a C0 control char (U+0008, via a yaml escape in the source template) through quote_yaml_double and asserts it survives byte for byte. confirmed it fails on the pre-fix naive quote (raw control char makes the SKILL.md unparseable, yaml ReaderError) and passes with the fix. addresses copilot review feedback on the PR.
|
good call. added a second round-trip (fcdbbcd) that feeds a C0 control char (U+0008, via a yaml escape in the source template) through quote_yaml_double and asserts it survives byte for byte. confirmed it fails on the pre-fix naive quote (raw control char makes the SKILL.md unparseable, yaml ReaderError) and passes with the fix. |
fixes #3391
the skills setup path builds SKILL.md frontmatter by hand and its
_quotehelper only escaped backslash and quote. a double-quoted yaml scalar can't carry a raw newline or control character, so a multiline (block-scalar) description reparsed with its newlines collapsed to spaces, and a control character broke yaml loading entirely. the description comes from the template frontmatter, so any command with a block-scalar description hits the multiline corruption.this is the SKILL.md sibling of the toml escaping in #3341 and the goose yaml issue #3382 — a different code path in the same file, plus the identical
_quotetwin in the hermes integration.fix: add a shared
specify_cli/_yaml_string.quote_yaml_doublehelper that emits a valid double-quoted scalar (newline/cr/tab short escapes, other control chars as\xXX) and route both renderers through it, so there's one implementation to keep correct.added a regression test that round-trips a multiline description through the yaml parser. i confirmed it fails on the pre-fix code by stashing the source and re-running (
first line\nsecond line->'first line second line '); it passes with the fix, and the skills suites stay green.