Skip to content

fix: escape Rich markup in workflow resolve output - #3879

Open
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/overlay-rich-markup
Open

fix: escape Rich markup in workflow resolve output#3879
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/overlay-rich-markup

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

specify workflow resolve <id> prints its two summary sections through console.print, which has Rich markup enabled. Neither line escaped its interpolated fields.

1. The tier label never rendered — on every invocation.

f"  • [{layer.tier}] {layer.source} (priority={priority})"

Rich parses [base] and [project-overlay] as style tags, so the bracketed tier is swallowed. This needs no untrusted input; the column has simply never worked:

  •  project:ov1 (priority=10)     <- tier label missing
  •  base (priority=n/a)           <- tier label missing

2. Step IDs reach Rich as markup.

Step IDs come from base-workflow / overlay YAML and are validated only against : (_parse_edit in overlays/schema.py) — brackets pass validation. Two failure modes:

  • Balanced [stuff] is silently swallowed, so new[stuff] prints as new and the step appears to vanish.
  • Unbalanced [/red] raises rich.errors.MarkupError → uncaught traceback, exit 1. The workflow can't be inspected at all:
$ specify workflow resolve wf
...
Step attribution:
  • a: base
MarkupError: closing tag '[/red]' at position 7 doesn't match any open tag

Fix

Route the interpolated fields through rich.markup.escape and escape the literal tier bracket as \[. This matches the pattern already used by workflow info's step graph and workflow_list's \[disabled].

Display-only: the returned payload was already unescaped and is unchanged, so machine-readable callers see no difference.

Tests

Three regression tests in TestOverlayCli, all verified to fail without the fix:

  • test_workflow_resolve_prints_tier_labels — asserts [base] and [project-overlay] appear in output
  • test_workflow_resolve_escapes_rich_markup_in_step_id — parametrized over new[stuff] (swallowed) and new[/red] (crashes)

tests/workflows/test_overlay_commands.py passes 23/23. The wider tests/workflows/ suite shows 10 pre-existing failures, all symlink-permission tests that fail identically on a clean tree on Windows without elevation.

🤖 Generated with Claude Code

`workflow resolve` printed two lines through `console.print`, which has
Rich markup enabled, without escaping:

1. The layer tier was wrapped in literal brackets:
   `f"  • [{layer.tier}] {layer.source} ..."`. Rich parsed `[base]`
   and `[project-overlay]` as style tags, so the tier label was swallowed
   on *every* invocation -- no untrusted input required. The column has
   never rendered.

2. Step attribution interpolated `composed.step_id` raw. Step IDs come
   from base-workflow / overlay YAML and are only validated against `:`
   (see `_parse_edit`), so brackets pass validation. A balanced
   `[stuff]` is silently swallowed; an unbalanced `[/red]` raises
   `rich.errors.MarkupError`, producing an uncaught traceback and exit 1
   -- the workflow cannot be inspected at all.

Route the interpolated fields through `rich.markup.escape` and escape
the literal tier bracket as `\[`, matching the existing pattern in
`workflow info`'s step graph and `workflow_list`'s `\[disabled]`.

Only display is affected; the returned payload was already unescaped and
is unchanged.

Adds 3 regression tests, all of which fail without the fix: the tier
label renders, and a step ID survives both the swallowing and the
crashing markup cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Assisted-by: Claude Opus 5 (1M context)
@Noor-ul-ain001
Noor-ul-ain001 requested a review from mnriem as a code owner July 31, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant