fix(integrations): escape control characters in goose recipe YAML renderer#3384
fix(integrations): escape control characters in goose recipe YAML renderer#3384marcelsafin wants to merge 2 commits into
Conversation
…derer YAML forbids C0 control characters (except tab and newline) and DEL in every scalar form, and a bare CR acts as a line break inside a block scalar. _render_yaml wrote the body verbatim into a |2 literal block scalar, so such bodies produced recipes the YAML parser rejects. Detect block-scalar-unsafe characters and fall back to an escaped double-quoted scalar via yaml.safe_dump, mirroring the TOML renderer's fallback strategy from github#3341. Fixes github#3382 Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR fixes Goose recipe YAML generation in YamlIntegration._render_yaml so command bodies containing C0 control characters (excluding tab/LF), DEL, or a bare carriage return no longer produce YAML that yaml.safe_load rejects.
Changes:
- Add a precompiled control-character detector and, when triggered, render
promptas an escaped double-quoted scalar viayaml.safe_dumpinstead of a|2literal block scalar. - Add regression tests ensuring both control characters and a bare
\rround-trip through YAML parsing.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/specify_cli/integrations/base.py |
Adds block-scalar “unsafe” detection and a safe_dump-based quoted-scalar fallback for prompt to keep generated Goose recipes parseable. |
tests/integrations/test_integration_base_yaml.py |
Adds regression coverage to ensure YAML rendering remains parseable and the prompt round-trips for control chars and bare CR. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| prompt_yaml = yaml.safe_dump( | ||
| {"prompt": body}, allow_unicode=True, default_style='"', width=float("inf") | ||
| ).strip() |
There was a problem hiding this comment.
Switched to sys.maxsize in ff0afae. For reference, PyYAML 6.0.3 accepts float("inf") (the emitter only compares against best_width) and produces byte-identical output, but an int is the documented type and avoids the question entirely.
Co-authored-by: Copilot <[email protected]>
Description
Fixes #3382
YamlIntegration._render_yamlwrites the command body verbatim into a|2literal block scalar. YAML forbids C0 control characters (except tab and newline) and DEL in every scalar form, and a bare CR acts as a line break inside a block scalar, so a body containing any of these produced a goose recipe the YAML parser rejects (unacceptable character #x0008: special characters are not allowed).Root cause: the block-scalar path has no escaping mechanism at all; there is no string form in YAML that carries these bytes raw.
Fix: detect block-scalar-unsafe characters with a precompiled character class and fall back to an escaped double-quoted scalar rendered by
yaml.safe_dump, which escapes them as\b,\r,\x7Fand so on. Bodies without such characters keep the existing readable block-scalar output byte for byte. This mirrors the fallback strategy #3341 gives the TOML renderers for the same bug class (#3340).Testing
uv run specify --helpuv sync && uv run pytest— 3776 passed, 105 skippedTwo regression tests in the shared
YamlIntegrationTestsmixin (exercised via the goose integration): control characters (\x08,\x0c,\x1b,\x7f) and a bare CR, both asserting the rendered recipe parses and the prompt round-trips. Both fail onmain.AI Disclosure
Bug found, patch and tests written with GitHub Copilot CLI; reproduction and full test suite run and verified locally by me.