Skip to content

Keep the Guard's Quote Mask When an Escape Sits Outside the Loop Condition - #2081

Merged
ptr727 merged 7 commits into
developfrom
feature/auto-1998
Sep 29, 2026
Merged

ptr727 merged 7 commits into
developfrom
feature/auto-1998

Conversation

@ptr727

@ptr727 ptr727 commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Summary

The wait-loop rule's quote mask read no escapes, so a backslash anywhere in a command misaligned it, and a quoted ";" inside a bounded test was then read as a separator. That denied the three bounded loops in #1998.

  • _marked_lex is an escape-aware lex that matches POSIX shlex token for token, and _quoted_mask now uses it.
  • Wherever bash reads syntax that lex does not model, the mask falls back to the earlier quote-keeping mask (_quote_kept_mask, moved verbatim), so the verdict there equals develop's. That covers a backtick outside single quotes, $(/${ inside double quotes, $', a word-start #, and, per decision Decide How the #1998 Quote-Mask Fix Proceeds After Its Review Budget #2060, any command holding a << other than <<< and every loop inside a sh -c/bash -c payload, since a heredoc fed to a shell and a payload are each read twice.
  • Self-test rows: the three Wait-Loop Bound Check Drops Its Quote Mask on Any Escape in the Command #1998 repros and a '\'' case are allowed and fail with the fix reverted, and eight deny rows pin every fail-open shape the local reviews found, each confirmed to loop under bash.

Verification

Pre-existing defects found along the way are filed as #2058, #2059, #2080, and a comment on #2064.

Closes on promotion: #1998

🤖 Generated with Claude Code

ptr727 and others added 7 commits September 28, 2026 23:12
…ition

The wait-loop rule recovered each token's quoting from a second,
quote-keeping lex that read no escapes, so a `\;`, an escaped double
quote, or a `'\''` anywhere in the command broke its token alignment and
dropped the mask for every loop. A quoted `";"` operand then read as a
separator and ended the test before its bound, denying a bounded loop.

The mask now comes from a lex that marks quoting while reading quotes and
escapes the way the shell tokens' own POSIX lex does, and it is still
dropped wherever its tokens differ from the shell tokens.

Closes on promotion: #1998

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The escape-aware lex aligned on commands the earlier quote-keeping lex
never did, and on two shapes its POSIX reading differs from bash's: a
backtick, `$(`, or `${` opening inside double quotes, where bash nests
quotes, and a `$'...'` string, where a backslash escapes a single quote.
A token bash reads as syntax was then marked a literal operand, turning
a denied unbounded loop into an allowed one.

On those shapes the mask now comes from the earlier quote-keeping lex,
so no verdict moves there, and the escape-aware mask is used everywhere
else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A backtick glued to a quoted word, or a `#` comment, is read by bash in
a way the escape-aware lex does not model. With an escape elsewhere, the
lex now aligned on such commands and marked a substitution's backtick or
a quoted `;` before a comment as a literal operand. The test then stayed
open and a comparison inside the substitution or the comment counted as a
bound.

A backtick outside single quotes and a `#` starting a word now take the
earlier quote-keeping mask as well. The docstring states that invariant
rather than describing the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A heredoc fed to a shell and a `bash -c` payload are each read twice, and
the first read unescapes text the escape-aware lex reads only once. Any
command holding a `<<` other than `<<<`, and every loop read inside a shell
wrapper's payload, now takes the quote-keeping mask, so its verdict equals
the earlier one. Two deny rows pin both shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `_marked_lex` docstring named only a heredoc fed to a shell, while the
check raises on any `<<` outside a `<<<`, quoted or in arithmetic too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only an unquoted heredoc's body is unescaped on the first read, so the
docstring's stated reason now names that form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 29, 2026 16:33
@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 40433eb7-fb97-47f8-bf0a-f1c6139ff36d


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (develop@b56efb2). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #2081   +/-   ##
==========================================
  Coverage           ?   55.91%           
==========================================
  Files              ?       16           
  Lines              ?     7359           
  Branches           ?        0           
==========================================
  Hits               ?     4115           
  Misses             ?     3244           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 55.91% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It reworks the core lexing logic of a security-sensitive write-safety guard where a subtle fail-open would be high-impact, so it warrants final human review despite the extensive verification.

Review effort: Balanced
Findings: None

What changed in this PR

This PR fixes a false-deny regression in the agent write-safety guard (gh-write-guard.py, requirement 7, the unbounded-wait-loop rule). Previously, the quote mask that lets the guard tell a quoted operand (e.g. ";") from a real separator was recovered by a second lex that read no escapes, so any backslash anywhere in a command misaligned the mask and dropped it for every loop, wrongly denying three bounded loops. The change introduces an escape-aware lexer that reproduces the POSIX shlex token stream while tracking per-token quoting, and falls back to the prior escape-blind mask wherever bash reads syntax that shlex does not model, preserving develop's verdict on those shapes.

Changes:

  • Add _marked_lex (escape-aware tokenizer with a marked flag) plus the _UnmodeledSyntax sentinel; rewrite _quoted_mask to use it and factor the old escape-blind mask into _quote_kept_mask.
  • Fall back to the quote-kept mask for unmodeled syntax (backtick, $(/${ inside double quotes, $'…', word-start #, any << heredoc) and for recursion into sh -c/bash -c payloads that the outer lex already unescaped.
  • Add 12 self-test rows: the three issue repros and a '\'' case (allow), plus eight deny rows pinning fail-open shapes surfaced during review.
File Description
host-setup/​agent-safety/​claude/​gh-write-guard.py Adds escape-aware _marked_lex/_UnmodeledSyntax, reworks the quote-mask helpers, gates the payload mask by recursion depth, and pins the new behavior with self-test rows.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

ptr727 added a commit that referenced this pull request Sep 29, 2026
…d Effort (#2104)

## Summary

Copilot code review's default effort moved from Lite to Balanced on
2026-09-28. Balanced rounds write the `ccr-overview-v2` body with a
per-file table and almost never a coverage statement. So `pr_review.py`
read every such pull request as `COVERAGE_IS_UNSTATED` (exit 45) and
parked it on a maintainer decision (#2103, #2095, #2089, #2082, #2077).

Measured over the Copilot rounds on the 40 newest pull requests here, 25
of 29 Lite rounds carried the `fleet-review` marker, against 1 of 22
Balanced rounds. Stating the marker literally in the runbook did not
help. A Balanced round on this branch read that instruction, quoted it
in its own summary, and still left the marker out. So the tooling now
accepts a coverage reading that works at any effort level, instead of
depending on one setting.

### The rule

A head that no coverage statement reaches, meaning none is stated on it
and none is carried to it, now reads `coverage=table` and passes when
all of these hold:

- A round covering the head has its own file table naming exactly the
changed files.
- No Copilot round on the pull request, on any commit, states or appears
to state partial coverage. The scan reads each round body in 12 ways:
raw, entity-decoded, and compatibility-folded, each with tags kept and
stripped three ways. A partial found in any reading counts.
- The whole review history is in view.
- The changed-file list is present, well-formed, non-empty, and within
the 100-file window.

A statement that reaches the head always wins, so a stated partial still
blocks. A pull request that ever had a partial round goes to the
maintainer. When no table stands in, the digest prints the reason as `NO
FILE TABLE STANDS IN: <reason>`.

The rule is stated in `GOVERNANCE.md` "PR Review Etiquette", in the
`pr-review-conduct` Merge Gate (source and generated copies), in the
runbook, in `scripts/README.md`, and in the script's own docstring and
exit-45 message.

Measured live with this branch: #2102, #2094, #2088, #2081, #2076,
#2050, #2046, #2039 and #2033 read `coverage=table`. #2098, which
carries the marker, still reads `full`.

## Known limits, all failing closed unless noted

- **Glob rows.** Copilot sometimes collapses generated skill copies into
glob rows such as `.../skills/*/references/*.md`, so on those pull
requests the table does not match and they go to the maintainer. #2053
and #2021 are examples.
- **Marker quoted near digits.** A round that quotes the marker next to
digits reads as a partial. That includes this pull request's own first
round, so this PR goes to the maintainer.
- **Early-closed fences.** A fence that Markdown closes early, through a
list item or an HTML block, masks a real table after it.
- **Prose partials (fail-open).** A partial worded in prose that none of
the patterns anticipate can still pass. The scan narrows this gap but
cannot close it.
- **Malformed marker before a full one (pre-existing, not introduced
here).** On the head, a malformed marker followed by a well-formed full
marker is read as full by the existing coverage reader.
- **Unpinned readings.** Tests do not pin each of the 12 readings
individually, the echoed snippet can carry raw control characters into
the digest line, and two docstrings overstate slightly. None of these
affects a verdict.

## Verification

- `python3 -m unittest discover -s tests`: 1841 tests pass. The new
tests were mutation-checked, and each guard fails its test when removed.
- ruff, mypy, the prose gate, markdownlint, `build_dist.py --check`,
`spec/validate.py`, and the eol gate are clean.
- The local strict review ran across several rounds, and the last pass
found no fail-open. Its differential fuzz over 150k bodies found nothing
the previous commit flagged that this one misses. Its receipt is
recorded.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727
ptr727 merged commit 9b99dd3 into develop Sep 29, 2026
11 checks passed
@ptr727
ptr727 deleted the feature/auto-1998 branch September 29, 2026 21:30
ptr727 added a commit that referenced this pull request Sep 29, 2026
…pilot Coverage at Balanced Effort (#2105)

## Summary

Promotes develop to main, carrying 14 pull requests:

- [#2076](#2076) Read the
Guard's Token Fallback With a Bash-Aware Context Scan
- [#2081](#2081) Keep the
Guard's Quote Mask When an Escape Sits Outside the Loop Condition
- [#2094](#2094)
Reconfigure pr_review.py's Console Streams to UTF-8
- [#2088](#2088) Guard
check_settings' Live Read of the Repository Response
- [#2102](#2102) Skip a
Blockquoted List Marker in the Prose Gate's Dash Rule
- [#2104](#2104) Accept a
Head Round's Exact File Table as Copilot Coverage at Balanced Effort
- [#2098](#2098) Quote the
Review Engine's Remaining Bare Shell Placeholders
- [#2050](#2050) Hand
Unstated Coverage to the Maintainer Instead of Prescribing a Re-Request
- [#2046](#2046) Report a
Copilot Review Request That Records Nothing Instead of Waiting It Out
- [#2053](#2053) State the
Python Build Profile by Its Trait, Not One of Its Forms
- [#2068](#2068) Strip the
CR From configure.sh's Environment-Name Encoding
- [#2033](#2033) Bound the
Guard's Stdin Descriptor Before int()
- [#2036](#2036) Guard
ruleset_id() Against Malformed Array Elements
- [#2039](#2039) Strip the
CR From configure.sh's Workflow-Model Read

The lead change is #2104. Copilot code review's default effort moved
from Lite to Balanced on 2026-09-28, and Balanced rounds write a
per-file table but almost never a coverage statement. `pr_review.py` now
accepts a head round's file table as coverage when it names exactly the
changed files and no Copilot round on the pull request has ever stated
or appeared to state partial coverage. Before this, every pull request
reviewed at Balanced effort parked on a maintainer decision. The other
pull requests are guard, configure, prose-gate, and review-engine fixes
that were waiting on that decision or landed earlier.

#2076 leaves #1910, #1958, and #1959 open, per #1960.

Closes #1998
Closes #2091
Closes #1260
Closes #1190
Closes #1652
Closes #2047
Closes #1904
Closes #1947
Closes #1882
Closes #2028
Closes #1987
Closes #1881

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

2 participants