fix(themes): scope-filter claims before they reach a cluster - #659
Merged
plind-junior merged 3 commits intoJul 30, 2026
Conversation
detect_themes filtered claims on status and approved_by but never on ArtifactScope, so a private or cross-project claim contributed its id -- and the session that produced it -- to the returned ThemeCluster. every other read surface (search, context, recall, salience, explain_ranking) runs its claims through scoping.is_visible first. the leak does not stop at the response: propose_theme writes claim_ids and session_ids straight into the theme page body, so an approved theme turns them into committed yaml readable by every viewer. viewer resolves from config by default, matching salience/recall, with an optional explicit ViewerContext for callers that already carry one. fixes vouchdev#657
plind-junior
enabled auto-merge
July 30, 2026 19:03
plind-junior
enabled auto-merge
July 30, 2026 19:05
plind-junior
enabled auto-merge
July 30, 2026 19:30
plind-junior
disabled auto-merge
July 30, 2026 20:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
themes.detect_themesnow filters its candidate claims throughscoping.is_visiblebefore they can reach aThemeCluster. The viewerresolves from config by default (
scoping.viewer_from, the sameVOUCH_PROJECT/VOUCH_AGENT/retrieval.scope/kb.idprecedenceevery other read surface uses); callers that already carry a
ViewerContextcan pass it as the new keyword-onlyviewerargument.Why
The detector filtered on status (
archived/superseded/redacted)and on
approved_by, but never onArtifactScope. Every other readsurface —
kb.search,kb.context,kb.recall, the salience sidebar,kb.explain_ranking— runs its claims throughis_visiblefirst; themeswas the one that did not.
A
ThemeClustercarriesclaim_idsandsession_ids, so an unfilteredscan handed a caller the ids of private and cross-project claims it could
not fetch, plus the sessions that produced them. Confirmed with a repro:
two sessions of claims all stamped
visibility: private, agent: alice,read by a viewer for whom
is_visiblereturns false on every one of them,still produced
ThemeCluster(entities=['auth', 'jwt'], claim_ids=['auth-mw', 'auth-uses-jwt-for-token-validation'], ...).What makes this worse than an advisory read:
propose_themewritescluster.claim_idsandcluster.session_idsstraight into the theme pagebody it files, so on approval the leaked ids become committed yaml in
pages/— readable by every viewer, regardless of the scope stamped on theoriginal claim. Fixing the detector closes the write path too, since
detect_themesis where clusters come from.Same fix class as #624 (salience excluding retracted claims from the reflex
sidebar) and #640 / #650 (explain_ranking withholding what the viewer and
the status gate already hide) — one more read surface brought in line with
what
kb.searchandkb.contextalready refuse to return.Fixes #657
What might break
Nothing on disk: no file moves, no field changes shape, no audit-log or
bundle format change, and no
kb.*method signature changes (the newviewerargument is keyword-only with aNonedefault that reproducestoday's config-resolved behaviour).
Behaviourally,
kb.detect_themesnow returns fewer clusters in exactly onecase: a KB whose claims carry scopes the reading viewer cannot see. A KB
where every claim is
public/team/ unbound-project— the defaultstamp for a single-project KB — gets byte-identical output. A multi-project
or agent-private KB that was previously getting cross-scope clusters will
see those clusters shrink or disappear; that is the fix.
propose_themeis unchanged. It still validates whatever cluster it ishanded, so a caller that hand-assembles one is not newly restricted.
VEP
Not applicable — no object model,
kb.*method surface, on-disk layout,bundle format, or audit-log shape change. A read-path filter brought in
line with the other retrieval surfaces.
Tests
make check-equivalent locally:ruff check src testsclean,mypy srcclean on the touched files (the only errors are the pre-existingWindows-only
resource.RLIMIT_*ones instrategy.py), fullpytest tests/ --ignore=tests/embeddingsgreentests/test_themes.py, each verified to fail ontestwithout theone-line filter:
test_detect_themes_excludes_claims_the_viewer_cannot_see,test_detect_themes_honors_explicit_viewer(both directions: theowning agent still sees its own claims, another agent does not), and
test_detect_themes_scope_leak_does_not_reach_a_theme_page(thedurable half — private ids must not survive into
propose_theme'spage body)
CHANGELOG.mdupdated under## [Unreleased]