Live disclosure leaks β reachable today
p0 #3283 The Hidden Connections feed card renders offer/need titles with no audience check β live on /api/feed/intelligence, and ungateable as written only client-reachable live leak Enterprise
use-feed.ts β /api/feed/intelligence serves offer/need titles from the denormalized connections table with no carrier pointer, so disclose() has nothing to rule on β ungateable as written. The other row sets in the same function are gated; connection_rows is not. The rows are written by ConnectionDetectionPhase with as_user=False during ingestion β correctly ungated at write time (no viewer exists then); the gate is owed at read time and absent. Enforcement is already on in production. The fix is a decision: give the table provenance (carrier message per side), or re-derive the node ids at read time and join messages. Sibling of #3141; related to #3128.
p0 #3155 traverse_graph renders the RAW canonical_name in a candidate's title at ABSTRACTED/ATTRIBUTED raw title, de-identified body Enterprise
The body is de-identified at ABSTRACTED/ATTRIBUTED but the title renders raw β _candidate_display_title returns display_name (falling back to canonical_name) unconditionally, computed before the disclosure level is read, so a below-floor viewer reads the real item name. Applies to every candidate _format_graph_candidates renders (hop 1, hop 2, retrieval), not one path. Sibling surfaces do not have this shape β _gated_node_text / _format_connection_pairs degrade the one string they render.
p0 #3128 TypedItemMaterializer reduces a multi-message node's audience to one arbitrary carrier (stable_min_uuid) β under-gating live on Regina one arbitrary carrier under-gates Enterprise
A multi-message node's audience is reduced to one lexically-minimum carrier (stable_min_uuid), so 25 Regina pairs are under-gated and 82 are incomparable β 35% have no most-restrictive contributor to pick. Reconstructed from conversation_questions: 155 groups span more than one distinct message, 232 contributing audiences discarded. The fix is one of: intersect the scopes, fail closed on disagreement, or stop grouping across conversations. The grouping also merges semantically unrelated short questions into one node (one node has 13 contributing messages). Blocks the obvious ANSWER fix in #3118.
p0 #2905 AMA: facts fail OPEN on missing audience while messages fail CLOSED β conversation-anchored facts skip disclosure facts fail open, messages closed All
The foundational asymmetry: facts fail OPEN on a missing audience β 100% of answer and 43.1% of offer nodes have no message anchor β while messages fail CLOSED. Sampled 400 conversation-anchored facts: none carry source_message_id. Of 4,489 conversations with more than one scoped message, 3,921 (87.3%) have mixed audiences, so most-restrictive aggregation collapses toward the narrowest audience β one HR message would mute every fact deduced from it (that 87.3% is an upper bound; tighten before relying). The fix is write-side (record per-fact evidencing messages) with a backfill for the existing facts, so it needs an ADR.
p1 #3325 A document's audience can be decided by an arbitrary carrier message (ADR-145 D4) arbitrary carrier decides document audience All
A document's audience falls back to an arbitrary carrier message β a COALESCE fallback that bites documents carrying no document-level scope; document_scope wins when present, and sensitivity_tier is document-level and unaffected. ADR-145 D4 already decided most-restrictive-wins (the safe failure direction); the attachment recovery backfill grows the affected population, so it cannot ship undecided.
p2 #3284 Three of the four connection matchers are ungated on the audience axis β and nodes_v2 RLS has no audience predicate to fall back on three matchers ungated on audience Latent
Three of four connection matchers read node text with no audience gate, and nodes_v2 RLS has no audience predicate to fall back on β the "RLS covers it" defence does not hold, because RLS gates tenant and tier, never audience. problem_solution.py and duplicate_work.py read node text with no as_user and no audience parameter. HTTP-reachable by any authenticated member, with no shipped UI caller. The #3141 fix closed the semantic-connections matcher; these three remain.
p1 #2824 business_context ships to the client ungated by disclosure level business_context bypasses the gate Latent
business_context β document_purpose, contains_pii, contains_financial_data and a 300-char summary β ships on the SSE wire outside the disclosure gate that shapes caption. Nothing renders it today, so the next UI inherits the leak; it is an over-broad payload rather than a rendered leak. The population changed in #2822, which now emits Office documents β the highest-signal class.
The rule, not an instance
p1 #3141 No choke-point for 'a serving traversal's rows must clear an audience check' β 10 call sites, 2 confirmed holes no choke-point, ten call sites Dev only
The rule has no choke-point β 10 call sites, one rule; five of them are gated only because a human remembered. PR #3290 closed the semantic-connections hole; the rule itself is unresolved. Measured: get_open_asks reaches 7,531 of 7,971 restrictive ask rows; the bridge pool draws on 41,490 of 74,660 restricted personβnode pairs. Options: gate at the repository boundary (as_user=True carries the gate), a check.py AST hook, or a typed carrier. Recommendation: the AST hook first, the repository-boundary gate as the real fix.
p1 #3118 ADR needed: an ANSWER node's own text is permanently ungateable under disclose()'s NULL-provenance model answer text permanently ungateable Dev only
All 2,023 answer nodes have source_message_id NULL β AnswerProperties has no scope field and the sole writer calls materialize_node without it, so every projection is COALESCE(NULL, NULL). ANSWER text β full verbatim conversation text β always takes disclose()'s structural passthrough. The ADR must decide three things: carry provenance (materializer change + backfill), or exclude verbatim-content types from the passthrough and fail closed, and what happens to the existing 2,023 rows with no recoverable audience. #3128 blocks the obvious fix.
Over-suppression Β· discharged
p2 #3285 disclose_current_viewer() is SECURITY INVOKER, so a viewer whose own job_function node sits above their tier resolves to no audience identity and is shown nothing entitled viewer shown nothing Enterprise
disclose_current_viewer() is SECURITY INVOKER, so 12 Regina edges resolve to no audience identity and an entitled viewer is shown nothing β over-suppression, not a leak. 2,106 role edges, 12 above tier 0; job_function nodes sit 73 at tier 0, 3 at tier 1, 7 at tier 3 (the tier-3 materializer default is not what lands in practice), and project nodes are far more skewed β 117 of 260 at tier 3. Two rulings offered; the narrower is SECURITY DEFINER, matching its own dependency current_viewer_person_id().
p2 #3295 AMA-DISCLOSE became the largest disclosure queue in the repo without anyone deciding it is a track β it has no status page this page discharges the ruling Dev only
The track became the largest disclosure queue in the repo with no ruling it was a track; it asked for a ruling, a page, and a decision on the orphaned p0s. This page discharges that ruling β the owner ruled 2026-08-16 by ordering pages for all tracks. The orphaned p0 it named (#3155) now sits on this track. The issue stays open, its needs:ruling label still attached.