fix: redact git stderr before bounding in auto_improvement (#7333) - #7350
Conversation
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Redact-then-bound at each build site, plus a mutation-verified straddle test and a structural sweep, closes the leak class rather than one instance. Suggestions
[DESIGN-REVIEWED] d86ef88 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. Composing the review. First-Principles-Verdict: CONCERNS Every item is derived from issue #7333 and closed as a class in-app — but two counted same-defect siblings outside the app and a mechanism the description misnames deserve human eyes. What this change shipsIntent: stop a credential in git/gh stderr from escaping redaction when the error text is cut to a fixed length — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] d86ef88 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
|
🤖 Kiro Crew Auto-Pipeline [operator: CrysisDeu#0c98c3a2] Disposition for the First Principles CONCERNS on Finding: 3 same-class sites outside the app ( |
Audit note — part of this has already landed; the rest has notThis PR is not a duplicate and is not finished by anything on Already landed
Which parts
|
bf3a34a to
a696f66
Compare
a696f66 to
b172209
Compare
|
🤖 Kiro Crew Auto-Pipeline [operator: CrysisDeu#0c98c3a2] Disposition for the GPT BLOCKING finding on
|
Six error payloads in backend/commit.py, plus sibling log lines and error strings in backend/pr_watchers.py, spine/gate.py, spine/driver.py and profiles/github_repo/pr_recipe.py, sliced raw subprocess stderr to a fixed character bound BEFORE any redaction ran. The downstream redaction pass needs the full secret shape to match, so a credential straddling the bound leaked as an unrecognizable fragment. commit.py is reachable by construction: it passes an authenticated remote URL as git argv, and git echoes the full userinfo URL on an auth failure. Every site now uses security.redact_and_truncate(text, bound), which scrubs the full text first and bounds after, keeping each site's existing bound. gate.py is registered as a non-egress redactor call site (its RuntimeError surfaces only through the spine driver's registered sinks). Regression tests pin the fetch-failure site with a straddle layout (mutation-verified: the raw slice and a slice-then- redact reorder both go red) and a structural sweep pins the whole class across every non-test module of the app. Closes #7333
b172209 to
d86ef88
Compare
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
bolichen97
left a comment
There was a problem hiding this comment.
Auto-triage Tier 1: fix:, 12 files — applies redact_via_context before character bounds on all git/gh subprocess stderr paths in the auto_improvement backend, so a credential in an echoed remote URL cannot be sliced mid-match and escape downstream redaction passes.
Qualifies: fix: category, 12 files (<=20 cap), all 5 AI reviewer checks success on head d86ef88 with 0 annotations, Semgrep 0 annotations, no CodeQL open alerts, security checklist all-NO (adds redaction, does not modify auth/trust-boundary/sandbox/secret handling logic).
Summary
Closes #7333.
Subprocess stderr was sliced to a fixed character bound BEFORE redaction at 20 sites across the auto_improvement app. The downstream redaction pass needs a secret's full shape to match, so a credential straddling the bound leaked as an unrecognizable fragment. Reachability is by construction at the primary site:
backend/commit.pypasses an authenticated remote URL as git argv, and git echoes the full userinfo URL on an auth failure.Every site now redacts the full text first and bounds after, keeping each site's existing bound:
(proc.stderr or '')[:N]) →redact_and_truncate(proc.stderr or '', N):backend/commit.py(6),backend/pr_watchers.py(2),spine/gate.py(1),spine/driver.py(9),profiles/github_repo/pr_recipe.py(2).tail[0][:N]/err[0][:N]aftersplitlines()[-1:]) →redact_and_truncate(tail[0], N):backend/clone_setup.py(4),backend/pr_watchers.py:1340,profiles/github_repo/profile.py:974. Found by the pre-push review lanes — the issue's grep sweep cannot see the alias form. (backend/deps.py:114, the same alias site, was fixed upstream by PR #7316, merged 2026-09-01 — this PR's rebase dropped its duplicate and keeps main's version.)stderr[-400:]) →redact(text)[-400:]:spine/agent_runner.py:897and:1015. Redact-then-tail rather thanredact_and_truncatebecause the END of stderr carries the actionable error; a tail cut of already-redacted text can at worst split a redaction marker, never a secret.Posture registry:
clone_setup.py,profile.py, andgate.pyare new redactor call sites classified inNON_EGRESS_REDACTION_MODULES(source-side pre-passes; their strings reach output only through this app's registered sinks);deps.py's entry landed with PR #7316.commit.py,driver.py,pr_watchers.py,pr_recipe.py,agent_runner.pywere already registered sinks. Stale prose describing the old raw-slice behavior was updated inroutes.py,security_posture.py, and a test docstring. Per main's gate-side log-redactor ratchet (TestGateSideLogRedactorSpelling), the 10 converted sites that are LOG lines (driver.py×8,pr_recipe.py×2) use the companion-awareredact_log_via_context(text)[:N]spelling — same redact-before-bound order; non-log error payloads and the persisted ledger note keepredact_and_truncate.Deliberately out of scope:
spine/proposer.py:83's unboundedr.stderr.strip()— with no slice, a credential keeps its full shape, which downstream pattern-based redaction can still match (reason recorded in the sweep test).Overlap with PR #7316 (issue #7307): resolved — #7316 merged 2026-09-01 with the identical redact-then-bound fix for
deps.py:114, and this PR's rebase onto post-#7316 main dropped its duplicate hunk. The app-wide structural pin below still coversdeps.py(it scans every non-test module, and main's version passes).Tests
New
tests/test_stderr_redact_before_bound.py:materialize_queued_difffetch failure): a fake secret is laid out so the 160-char bound falls INSIDE it, premise-guarded (start < 160 < start + len(secret),@past the bound). Mutation-verified: reintroducing the raw slice fails 3 tests; a slice-then-redact reorder fails the straddle test (the cut drops the@hosttail the userinfo regex needs).tail[0][:N]/err[0][:N]alias forms, with the sanctioned redact-then-tail form excluded. The defect recurred file by file, so the sweep is what keeps the class closed.Local gates all green: black/subprocess-encoding/isort/flake8 gates,
mypy(1218 files clean), brand + harness-parity diff gates, full app test dir (1061 passed),test/test_security_posture.py(registry drift guards), and the sixtest/test_ai_*_coverage.pyfiles for every touched module (1006 passed). Full suite: 77,502 passed; the 92 failures + 2 errors reproduce identically on a pristine main checkout on this host (environment-inherited, verified via a detached main worktree).Pre-push review: GPT (gpt-5.6-sol) and Opus (claude-opus-5) lanes, one round each; all verified findings fixed (the alias and tail sites above), advisories addressed (stale posture prose, gate.py classification reason names the
GateResult/ledger travel path, sweep limits documented in-test).No UI change — backend only, no screenshots required.
Pattern harvest
Rule candidate: a fixed-character bound applied to subprocess output (stderr especially) BEFORE redaction defeats pattern-based redaction — a mid-value cut leaves a fragment no credential regex can match, in head-slice (
[:N]), tail-slice ([-N:]), and last-line-alias (splitlines()[-1:]thentail[0][:N]) forms. Flag any changed line that bounds subprocess output without a preceding redact pass; the sanctioned forms areredact_and_truncate(text, N)andredact(text)[-N:]. This PR ships the app-scoped structural pin (tests/test_stderr_redact_before_bound.py); the harvest candidate is promoting the same scan repo-wide (AUTOSDErecurring-defect-patternsor a semgrep rule), since the class already recurred across two issues (#7307, #7333) and eight modules.