MCP: harden cached report signatures #43
Labels
No labels
area:ci
area:docs
area:engineering
area:framework-fp
area:test-coverage
dogfood:fn
dogfood:fp
dogfood:friction
dogfood:tp
phase:b
phase:c
severity:critical
severity:high
severity:low
severity:medium
source:deepseek-v4-pro
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
pdurlej/fallow-py!43
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "codex/mcp-cache-signature-40"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Canary Context Pack
Product story
MCP agents should not receive stale pyfallow reports when a file or config changes inside the cache TTL but the filesystem metadata is too coarse to show it.
What changed
.pyfallow.tomlchanges with restored mtime.Why it changed
DeepSeek audit triage issue #40 identified that the MCP report cache used only
mtime_nsand file size. That can miss content changes on coarse filesystems or controlled metadata updates.Files touched
mcp/src/pyfallow_mcp/runtime.pymcp/tests/test_mcp.pyRuntime evidence
/tmp/pyfallow-verify-venv/bin/python -m pytest -q mcp/tests/test_mcp.py::test_cached_report_invalidates_same_size_source_content_change mcp/tests/test_mcp.py::test_cached_report_invalidates_same_size_config_content_changefailed becausecached_report()returned the stale cached object.python3 -m compileall -q src tests mcp/src mcp/testsgit diff --checkpython3 -m pytest -q/tmp/pyfallow-verify-venv/bin/python -m pytest -q mcp/testsPYTHONPATH=src python3 -m pyfallow analyze --root . --fail-on warning --min-confidence mediumKnown constraints
This intentionally keeps the current cache TTL and global-cache lifecycle. The hash is computed only for paths already included in the report signature: analyzed modules, scanned source-root Python files, dependency files, and the active config file.
Explicit out-of-scope
Requested decision
Approve if the content-aware invalidation is correct and scoped enough for #40.
Merge blockers
Closes #40.
Refs #35, #9.
Claude review brief for #43.
Please do one bounded code review pass, not an architecture proposal. Terminal action should be one of:
approve_merge,approve_with_evidence_gap, orrequest_changeswith concrete blockers only.Scope: focused MCP cache invalidation correctness for #40. This PR adds content-aware SHA-256 signatures to the existing cache signature and regression tests for same-size source/config changes with restored mtime.
Review focus:
cached_report()stop returning stale data when content changes butmtimeand size do not?.pyfallow.tomlconfig invalidation both covered?Do not block on:
Recommended local commands:
Keep review short. If the narrow invariant holds and CI is green, approve.
Mandatory non-author review (ADR 0010 / AGENTS.md)
Terminal action:
approve_mergeReviewed by
claude(Opus 4.7). PR opened bycodex, non-author requirement satisfied.Narrow invariant: does
cached_report()stop returning stale data?test_cached_report_invalidates_same_size_source_content_change— writes pre-parse-error source, callscached_report(), then writes same-length malformed source, restores mtime viaos.utime(ns=...), callscached_report()again → assertsparse-errorappears in second result andsecond is not first.pyfallow.tomlcontent change with restored mtime + same size invalidates?test_cached_report_invalidates_same_size_config_content_change— same shape, swapsdupes.mode = "mild"→"wild", mtime restored → assertsconfig-errorappears and result is fresh_path_signatureis only called by_report_signatureover_signature_paths(root, result)(unchanged); SHA-256 added inside that existing tuple, no new path scanninghashlibadded to runtime.py importsImplementation notes
(rel, mtime_ns, size)→(rel, mtime_ns, size, sha256_hex). Type annotation updated inREPORT_CACHEand both function signatures. ✅iter(lambda: handle.read(1024 * 1024), b"")) — bounded memory for large files. ✅OSError → "unreadable"sentinel — degrades gracefully without crashing on permission errors etc. The sentinel differs from"missing"(FileNotFoundError →(rel, -1, -1, "missing")) so the two error modes don't collide. ✅Local
python3 -m pytest -qin worktree: 79 tests pass, exit 0.Verdict
approve_merge. Narrow correctness invariant holds with behavioral tests as evidence. Scope discipline clean — no lifecycle/eviction creep into this PR.