feat(groom): extract CI-privileged patch path policy into tested patch_policy.py (BE-4404)#71
feat(groom): extract CI-privileged patch path policy into tested patch_policy.py (BE-4404)#71mattmillerai wants to merge 2 commits into
Conversation
…h_policy.py + close lockfile/.husky/action.yml gaps (BE-4404) The groom auto-builder's Capture patch step enforced its CI-privileged deny-list as an untestable inline `grep -E`, and still missed live execution vectors — the biggest being dependency lockfiles (a rewritten `resolved`+`integrity` pair runs an attacker tarball's postinstall in credentialed CI before human review). Extract the deny-list into `.github/groom/patch_policy.py` (denied_paths + a NUL-delimited stdin main(), exit 0 always), port the existing patterns verbatim, and add lockfiles, `.husky/`, composite-action manifests, `.gitmodules`, and the common build/test config across JS/Python/Rust/Ruby/Swift/Gradle/Bazel/CMake. Add a unit suite (positive root+nested, anchored negatives, git raw-byte quote/ newline regressions). Wire the step to `python3 "$GROOM_ASSETS/patch_policy.py"`, trim the stale comment to a pointer, and document the policy + its structural limit (source still executes in caller test CI) in the README and header. No change to bail semantics or the builder default; no caller touched.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 18 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 9 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 4 |
| 🟡 Medium | 4 |
| 🟢 Low | 1 |
Panel: 8/8 reviewers contributed findings.
…view panel (BE-4404) Address the cursor-review panel findings on the CI-privileged patch deny-list: - groom.yml: drop `|| true` on the patch_policy.py call. main() always exits 0 now, so `|| true` only masked real failures (missing $GROOM_ASSETS, bad interpreter, crash) — an empty TOUCHED_CI then fails OPEN. Under the step's `set -euo pipefail`, letting a failure abort fails CLOSED (no PR). - groom.yml: strip C0 control bytes from denied paths before interpolating them into the `::warning::` line — a path named `…\r::stop-commands::…` could inject workflow commands (the runner treats bare `\r` as a line terminator). - patch_policy.py main(): write raw bytes via surrogateescape so a non-UTF-8 denied path is emitted (and blocked), not crashed on by strict text stdout. - patch_policy.py: close under-blocks in the module's own "never under-block" direction — lowercase `makefile`/`rakefile`, `gradlew`/`gradlew.bat`, Go (`go.mod`/`go.sum`/`go.work`/`go.work.sum`), `Taskfile.yaml`+lowercase, `Package.resolved`, `.pnpmfile.cjs`, `.cargo/config[.toml]`, multi-Dockerfile globs. Match case-insensitively (macOS/Windows CI runners are). - patch_policy.py: `denied_paths` rejects a bare str/bytes (char-iteration footgun). - Tests + README updated to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ELI-5
The groom auto-builder lets a (prompt-injectable, credential-free) AI agent write a code patch, and a later job opens a review-gated PR from it. But review only gates the merge — pushing the branch already runs the repo's CI, with secrets, before any human looks. So if that patch edits a file CI executes, it's arbitrary code execution in credentialed CI. There was already a deny-list that downgrades such a patch from an auto-PR to a plain filed issue — but it was a one-line
grepnobody could test, and it missed the scariest case: dependency lockfiles. Rewrite a lockfile'sresolvedURL +integrityhash and the nextnpm cidownloads and runs an attacker's install script. This PR moves the deny-list into a small, unit-tested Python file and plugs that gap (plus.husky/,action.yml, and build files for many ecosystems).What changed
.github/groom/patch_policy.py(new) —denied_paths(paths)returns the CI-privileged subset of changed paths;main()reads NUL-delimited paths from stdin (matchinggit diff --cached --name-only -z) and prints matches, exit 0 always (the caller tests non-emptiness). Ports the pre-existing patterns verbatim, then adds: lockfiles (package-lock.json,npm-shrinkwrap.json,yarn.lock,pnpm-lock.yaml,bun.lock/bun.lockb,poetry.lock,uv.lock,Pipfile.lock,Cargo.lock,Gemfile.lock),.husky/(any segment),action.yml/action.yaml,.gitmodules,requirements*.txt,Pipfile,Cargo.toml,build.rs,Package.swift,*.pbxproj,*.gradle/*.gradle.kts/settings.gradle,gradle/wrapper/,Gemfile,Rakefile,*.gemspec,CMakeLists.txt/*.cmake, BazelWORKSPACE(.bazel)/BUILD(.bazel)/*.bzl,Jenkinsfile,Taskfile.yml,just(J)file..github/groom/tests/test_patch_policy.py(new) — 19 tests: every ported + added pattern family at root and nested depth, anchored negatives (src/foo.py,README.md,packages.json,Dockerfile.md, … must NOT match), and the git raw-byte regressions (embedded"and embedded newline still caught from-zbytes; both newline halves tested).groom.ymlCapture patchstep — the inlinegrep -Ebecomesgit -C repo diff --cached --name-only -z | python3 "$GROOM_ASSETS/patch_policy.py".GROOM_ASSETSis the existing workflow-env var pointing into the_groom_assetscheckout thebuildjob already does (Load groom assets (builder brief)runs before this step). The stale comment block is trimmed to a pointer keeping the two load-bearing notes (conservative default; NUL-delimited comparison mandatory), and the bail reason string now names the policy file.README.md+groom.ymlheader — mention the policy file and its structural limit: the deny-list guards privileged-config surfaces, but any patch's source still executes when caller CI runs its tests — callers should avoid exposing secrets to test steps and considernpm ci --ignore-scriptswhere viable.Verification
actionlint .github/workflows/groom.yml— clean (includes shellcheck on therun:block).python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'— 78 tests OK (ledger 59 + patch_policy 19);test_patch_policyandtest_ledgerboth auto-discovered by the existingtest-groom-scripts.ymlpattern, no workflow change needed.printf '.github/workflows/ci.yml\0package-lock.json\0src/app.py\0.husky/pre-commit\0tools/action.yml\0README.md\0' | python3 patch_policy.pyprints exactly the four privileged paths; clean-only input prints nothing; empty stdin → exit 0.Acceptance criteria
PortedPatternsTestpins the full ported set at root + nested).package-lock.json,.husky/pre-commit,tools/action.ymleach bail.bails → lands in the ledger and files agroomissue; nothing dropped.builder: falsedefault; no caller touched.Judgment calls
|| truekept on the assignment. The original wrapped thegrepin|| true(grep exits 1 on no-match, the common case).patch_policy.pyexits 0 always, so|| truenow only masks a genuinegit/python3failure — preserving the original posture that this line never aborts the step underset -euo pipefail. Dropping it would instead abort the matrix leg on any error, which (per the step's own secret-scan comment) drops the finding entirely rather than filing it. The script is unit-tested and SHA-pinned viaworkflows_ref, so a broken script is caught bytest-groom-scripts.ymlbefore merge. Flagged rather than silently changed..github/(workflows|actions)/stays root-anchored (verbatim port), while newly-added.husky/andgradle/wrapper/match at any depth per the ticket — documented inline. Over-blocking is the safe direction, but the ported pattern is kept byte-for-byte to guarantee no regression on the previously-covered set.Negative-claim falsification
Not applicable in the #581 sense: this diff does not deny a product capability or add a "not supported"/
STOPdead-end. "Denied" here means a builder patch is downgraded from an auto-PR to a filed issue — the finding is still delivered and the human authors the privileged change. Nothing is dropped. The deny behavior is empirically validated by the end-to-end pipeline simulation above, not merely asserted by self-authored tests.BE-4404