fix(memory): redact pip stderr before logging install failures (#7279) - #7283
Conversation
Route the decoded stderr of the faiss-cpu install and ensurepip bootstrap failure warnings through security.redact_and_truncate instead of a raw 500-char head slice. Redaction runs over the FULL decoded stderr before the length bound (the redact-before-bound invariant), so a private-index userinfo credential in pip's auth-failure output cannot reach the gateway log — not even as a fragment straddling the old slice boundary. Closes #7279
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Real leak, fixed at the root with the existing shared [DESIGN-REVIEWED] d0c0d82 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe change routes pip/ensurepip stderr through No findings. [OPUS-REVIEWED] d0c0d82 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
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: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. Composing the review. First-Principles-Verdict: CONCERNS The fix is real and uses the existing What this change shipsIntent: stop pip's private-index credential landing in the gateway log when a faiss/ensurepip install fails — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] d0c0d82 |
|
Disposition — First Principles CONCERNS: accepted-and-deferred, sibling extracted to a tracked issue.
Filed as #7307 with the lane's closed enumeration (17 Thanks to the lane for the genuine remainder — the enumeration is what made it fileable as a |
…eps (#7307) (#7316) * fix(auto_improvement): redact pip stderr before bounding in install_deps install_deps() returned pip's stderr tail line to the caller's error payload unredacted and bound-before-redact. The child pip inherits the gateway environment, so an authenticated private index reaches it; on an auth failure pip echoes the raw request URL (token included) to stderr, which then surfaced in the dashboard. Pass the full tail line through security.redact_and_truncate(.., 200) so redaction runs over the whole string before the 200-char bound, matching the redact-before-bound invariant. Same fix shape as handlers/memory.py (#7283). Add regression tests pinning that a credential-bearing stderr line, and one straddling the truncation boundary, never reach the payload. Fixes #7307 * fix(auto_improvement): allowlist deps.py, pin straddle test (#7307) Repairs the first revision of this PR (kiro-agent): - Register deps.py in NON_EGRESS_REDACTION_MODULES: the posture drift-guard (test_every_redactor_call_site_is_a_registered_sink_or_allowlisted) requires every redactor call site to be classified, and it red both Linux and Windows backend shards. deps.py is a source-side pre-pass, not an egress boundary: the payload is served only through routes.py, the registered sink. - Repair the straddle regression test: the prior layout put the token start at index 202, entirely past the 200-char bound, so the test passed even on the unfixed code. The token now starts at index 190 with a premise guard (assert start < 200 < start + len(token)), and the exact prefix fragment a bound-before-redact implementation leaks is asserted absent. Mutation-verified: raw slice fails both tests, slice-then-redact reorder fails the straddle test. - Split the over-long return line (black line-length gate) and make the code comment precise: the serving route does redact, but a mid-token cut breaks the credential-regex match, which is the reachable defect. Co-authored-by: Kiro Crew <noreply@kiro.dev> --------- Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Co-authored-by: Kiro Crew <noreply@kiro.dev>
kirodotdev#7283 _BASELINE_LOG_SITE_CENSUS has no entry for dashboard/handlers/memory.py; kirodotdev#7279/kirodotdev#7283 added two gate-side baseline-redactor sites there (pip ensurepip bootstrap + faiss-cpu install stderr, logged via logger.warning in the dashboard process) after the census was measured, so test_no_new_gate_side_log_line_reads_the_baseline_redactor fails. Raise the census to 2 per the kirodotdev#7492 re-measure pattern. No source change.
Summary
handlers/memory.pylogged pip/ensurepip subprocess stderr into the gateway log unredacted (stderr.decode()[:500]) at two warning sites. On the faiss-cpu install path the child pip inherits the gateway environment, so a private index configured with userinfo credentials reaches pip, and on an auth failure pip writes the raw request URL — including the token — to stderr, which then landed verbatim in the gateway log.Both sites now route the full decoded stderr through
security.redact_and_truncatebefore logging:_PIP_STDERR_LOG_CHARS.stderr.decode(errors="replace")on both touched lines so a non-UTF-8 pip stderr (Windows codepage, mangled index response) cannot raiseUnicodeDecodeErroron the failure path and wedge_embedding_setup_statusatinstalling_faiss(accepted pre-push Opus advisory; the site's own comments document that wedge class).Out of scope: the head-vs-tail anchor policy for which 500 chars to keep is tracked in #5555; this PR only introduces the missing redaction.
Tests
test/test_enable_embeddings_faiss.py::TestPipStderrRedaction(4 new tests, all mutation-verified red against the raw-slice code):[REDACTED: credential], raw token absent (end-to-end through the handler, subprocess mocked)._ensure_pip_availablepath.@the userinfo pattern requires.UnicodeDecodeError, warning still logged and redacted.Local gates (CI-pinned tools): black-baseline, isort, flake8, mypy (1216 files clean), brand-name, harness-parity, subprocess-encoding — all green. Targeted pytest: 23 passed.
Pre-push review dispositions
gpt-5.6-solhollow-returned;gpt-5.6-terrafallback): NO BLOCKING FINDINGS, evidence-backed.claude-opus-5): NO BLOCKING. Advisory 1 (strictdecode()crash on the failure path) accepted and fixed (errors="replace"+ regression test). Advisory 2 (redaction now scans the whole stderr buffer, O(n·matches) CPU) no action in-diff: the shape is shared by every existingredact_and_truncatecaller,create_subprocess_limitedalready rlimits the child, and pre-capping the input would re-introduce a bound ahead of redaction — the exact ordering this fix removes.Closes #7279
Pattern harvest
Rule candidate: semgrep
Pattern: subprocess stderr reaching a logging call without redaction —
logger.<level>(..., stderr.decode(...))(or a sliced variant) not routed throughsecurity.redact_and_truncate. This PR fixes two sites; unredacted siblings of the same shape exist elsewhere (e.g.voice_reply.py:364,chat_voice.py:360,apps/routes.py:3236) with lower credential exposure than the pip/index path but the identical mechanism, so a lint that flagsstderr.decodeflowing into a log call without the redaction shim would catch the class at review time.Supersedes #7292, which made the same fix but decoded with a bare
stderr.decode()(raises on non-UTF-8 pip stderr) and repeated a bare
max_chars=500at both call sites.