From 791c0a57a0b025c0a63887e954e1c01020cebec3 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Sat, 16 May 2026 22:52:06 -0700 Subject: [PATCH] review: gate web Reddit enrichment behind EXCLUDE_SOURCES PR #366 routes Reddit URLs found in web-search results through the public Reddit JSON API to recover thread body + top comments (the Claude Code WebFetch tool blocks reddit.com directly). That bypass is sound and the fixed problem is real - but the always-on shape ignores user intent on source gating. A user who sets EXCLUDE_SOURCES=reddit to suppress Reddit results would still get Reddit content smuggled back in via web-search URLs that happen to point at reddit.com threads. This contradicts the suppression contract that EXCLUDE_SOURCES is supposed to provide (see lib/pipeline.available_sources where the same env var gates the top-level Reddit source). Add a _reddit_excluded(config) check in web_search() that mirrors the parsing pattern from lib/pipeline (comma-separated, case-insensitive, whitespace-tolerant). When reddit is in EXCLUDE_SOURCES, skip the enrichment pass entirely - the web results themselves still flow through, but they're not augmented with Reddit body/comments. Four new tests in test_grounding_v3.py cover: - EXCLUDE_SOURCES=reddit skips enrichment - case-insensitive parsing matches REDDIT/Reddit/whitespace-padded/csv - Other sources in EXCLUDE_SOURCES don't trigger the gate - Enrichment runs normally when reddit isn't excluded 19/19 grounding tests pass. --- skills/last30days/scripts/lib/grounding.py | 17 +++++++++- tests/test_grounding_v3.py | 37 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/skills/last30days/scripts/lib/grounding.py b/skills/last30days/scripts/lib/grounding.py index 9d7835e..ad33ede 100644 --- a/skills/last30days/scripts/lib/grounding.py +++ b/skills/last30days/scripts/lib/grounding.py @@ -232,17 +232,32 @@ def web_search( raise ValueError(f"Unsupported web backend: {backend!r}") else: return [], {} - if items: + if items and not _reddit_excluded(config): items = _enrich_reddit_items(items) return items, artifact +def _reddit_excluded(config: dict) -> bool: + """Return True when EXCLUDE_SOURCES contains 'reddit'. + + Respects the same suppression knob the pipeline uses for source gating, + so a user who set EXCLUDE_SOURCES=reddit doesn't get Reddit content + smuggled back in via web-search URLs. + """ + raw = (config.get("EXCLUDE_SOURCES") or "").split(",") + return any(s.strip().lower() == "reddit" for s in raw) + + def _enrich_reddit_items(items: list[dict]) -> list[dict]: """Enrich web search results that are Reddit URLs with thread body and comments. Claude Code's WebFetch blocks reddit.com, so the model can't retrieve Reddit content from web search results. This fetches it via the public JSON API (reddit.com/.../.json) which bypasses that restriction. + + Callers should gate this with EXCLUDE_SOURCES=reddit handling (see + `_reddit_excluded`) so a user who explicitly excluded Reddit doesn't + get Reddit content via web-search URLs. """ from . import reddit_enrich diff --git a/tests/test_grounding_v3.py b/tests/test_grounding_v3.py index b9867d6..0a662f5 100644 --- a/tests/test_grounding_v3.py +++ b/tests/test_grounding_v3.py @@ -190,5 +190,42 @@ class WebSearchDispatchTests(unittest.TestCase): grounding.web_search("test", ("2026-02-25", "2026-03-27"), {}, backend="google") +class RedditEnrichmentGateTests(unittest.TestCase): + """EXCLUDE_SOURCES=reddit must suppress the web-search Reddit enrichment. + + Otherwise a user who explicitly excluded Reddit would still get Reddit + content smuggled back in via web-search URLs that happen to point at + reddit.com threads. + """ + + def test_reddit_excluded_via_exclude_sources_skips_enrichment(self): + config = {"BRAVE_API_KEY": "k", "EXCLUDE_SOURCES": "reddit"} + items = [{"url": "https://www.reddit.com/r/python/comments/abc/title/", "snippet": "original"}] + with patch("lib.grounding.brave_search", return_value=(items, {})), \ + patch("lib.grounding._enrich_reddit_items") as enrich_mock: + grounding.web_search("test", ("2026-02-25", "2026-03-27"), config, backend="auto") + enrich_mock.assert_not_called() + + def test_reddit_excluded_case_insensitive(self): + for value in ("REDDIT", "Reddit", " reddit ", "x,reddit,y"): + config = {"BRAVE_API_KEY": "k", "EXCLUDE_SOURCES": value} + self.assertTrue( + grounding._reddit_excluded(config), + msg=f"_reddit_excluded should be True for EXCLUDE_SOURCES={value!r}", + ) + + def test_reddit_not_excluded_when_other_sources_listed(self): + config = {"EXCLUDE_SOURCES": "tiktok,instagram"} + self.assertFalse(grounding._reddit_excluded(config)) + + def test_enrichment_runs_when_reddit_not_excluded(self): + config = {"BRAVE_API_KEY": "k"} + items = [{"url": "https://www.reddit.com/r/python/comments/abc/title/", "snippet": "original"}] + with patch("lib.grounding.brave_search", return_value=(items, {})), \ + patch("lib.grounding._enrich_reddit_items", return_value=items) as enrich_mock: + grounding.web_search("test", ("2026-02-25", "2026-03-27"), config, backend="auto") + enrich_mock.assert_called_once() + + if __name__ == "__main__": unittest.main()