Skip to content

fix(themes): scope-filter claims before they reach a cluster - #659

Merged
plind-junior merged 3 commits into
vouchdev:testfrom
philluiz2323:fix/themes-viewer-scope
Jul 30, 2026
Merged

fix(themes): scope-filter claims before they reach a cluster#659
plind-junior merged 3 commits into
vouchdev:testfrom
philluiz2323:fix/themes-viewer-scope

Conversation

@philluiz2323

Copy link
Copy Markdown
Contributor

What changed

themes.detect_themes now filters its candidate claims through
scoping.is_visible before they can reach a ThemeCluster. The viewer
resolves from config by default (scoping.viewer_from, the same
VOUCH_PROJECT / VOUCH_AGENT / retrieval.scope / kb.id precedence
every other read surface uses); callers that already carry a
ViewerContext can pass it as the new keyword-only viewer argument.

Why

The detector filtered on status (archived / superseded / redacted)
and on approved_by, but never on ArtifactScope. Every other read
surface — kb.search, kb.context, kb.recall, the salience sidebar,
kb.explain_ranking — runs its claims through is_visible first; themes
was the one that did not.

A ThemeCluster carries claim_ids and session_ids, so an unfiltered
scan 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_visible returns 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_theme writes
cluster.claim_ids and cluster.session_ids straight into the theme page
body it files, so on approval the leaked ids become committed yaml in
pages/ — readable by every viewer, regardless of the scope stamped on the
original claim. Fixing the detector closes the write path too, since
detect_themes is 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.search and kb.context already 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 new
viewer argument is keyword-only with a None default that reproduces
today's config-resolved behaviour).

Behaviourally, kb.detect_themes now returns fewer clusters in exactly one
case: a KB whose claims carry scopes the reading viewer cannot see. A KB
where every claim is public / team / unbound-project — the default
stamp 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_theme is unchanged. It still validates whatever cluster it is
handed, 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 tests clean, mypy src clean on the touched files (the only errors are the pre-existing
    Windows-only resource.RLIMIT_* ones in strategy.py), full
    pytest tests/ --ignore=tests/embeddings green
  • New / changed behaviour has a test — three cases in
    tests/test_themes.py, each verified to fail on test without the
    one-line filter:
    test_detect_themes_excludes_claims_the_viewer_cannot_see,
    test_detect_themes_honors_explicit_viewer (both directions: the
    owning agent still sees its own claims, another agent does not), and
    test_detect_themes_scope_leak_does_not_reach_a_theme_page (the
    durable half — private ids must not survive into propose_theme's
    page body)
  • CHANGELOG.md updated under ## [Unreleased]

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
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance tests tests and fixtures size: S 50-199 changed non-doc lines labels Jul 30, 2026
@plind-junior
plind-junior enabled auto-merge July 30, 2026 19:03
@github-actions
github-actions Bot disabled auto-merge July 30, 2026 19:03
@plind-junior
plind-junior enabled auto-merge July 30, 2026 19:05
@github-actions
github-actions Bot disabled auto-merge July 30, 2026 19:15
@plind-junior
plind-junior enabled auto-merge July 30, 2026 19:30
@plind-junior
plind-junior disabled auto-merge July 30, 2026 20:24
@plind-junior
plind-junior merged commit 6bfc593 into vouchdev:test Jul 30, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs documentation, specs, examples, and repo guidance size: S 50-199 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: kb.detect_themes leaks claim and session ids the viewer cannot retrieve

2 participants