Mindbridge Β· track status

AMA-DISCLOSE

Content reaches someone who should not see it β€” a failure nobody experiences until it is catastrophic.

Re-measured 2026-08-16 Β· the open count resolves live at build. Verify each issue is still open before starting it β€” this queue moves without notice. Never published; publish fresh per docs/status/artifacts.json.

Next β†’#3283 the Hidden Connections feed card β€” the only client-reachable live leak (offer/need titles rendered with no audience check). Next decision: the fact-open asymmetry #2905 (ADR + backfill), with #3118 as the design question.
7 live disclosure leaks Β· 2 model-call leaks Β· 2 gated on a ruling Β· 1 over-suppression Β· 1 discharged
8Open issues
2,023Answer nodes Β· NULL provenance
43.1%Offer nodes Β· no message anchor
82Regina pairs Β· incomparable audiences
Live leaks Model-call leaks The rule Suppressed Β· discharged Why this order Rulings History

Open work

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.
Leaks through a model call
p0 #3135 The batched scoring prompt shows the LLM every retrieved candidate ungated; only the winners are gated at render model sees every candidate ungated Enterprise
The scoring prompt shows the model every retrieved candidate ungated (up to 400 chars each, unredacted); only the winners are gated at render, so a losing restricted candidate still shapes the rationale. The only guard between the two is a soft prompt instruction, not a structural gate. The remaining half after PR #3134 gated the profile; the shared base.py::reason() pipeline means all five reasoning primitives route the same way. Gating before scoring means building the disclosure map earlier β€” one extra evaluation batch per call; current_returned_count semantics need care (candidates fed to the model but never rendered are not "returned", and that number is durable SOC 2 evidence). Not measured: no census of how often a withheld candidate co-occurred with a rendered winner.
p1 #3117 FindBridges feeds ungated profile content into the LLM that writes the rendered rationale ungated profile feeds the rationale Enterprise
The rendered rationale was written by an LLM shown ~40 ungated profile nodes β€” default_traversal_edges() does not exclude ASKED/ANSWERED, so full-verbatim QUESTION/ANSWER text is reachable. PR #3134 gated the profile half; the retrieved-candidate half is #3135.
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.

Why is it in this order?

Why is it in this order?

The criterion, top to bottom β€” each rung breaks only the ties the one above leaves.

  1. Live disclosure leak, reachable today. Content a viewer can be served or shown now that they may not read. The owner ruled the privacy boundary is the judge's output, not its input (2026-08-14), so a rendered leak outranks a prompt leak. Client-reachable outranks HTTP-only; both outrank content that ships but nothing renders.
  2. Leak through a model call. Content the model reads ungated and then writes into rendered output β€” below rung 1 (the output is the boundary), above suppression (the contamination survives into what is rendered).
  3. The rule, not an instance. The choke-point (#3141) and the owed ADR (#3118) each close a class, yet each awaits a ruling β€” gated work is sequenced by its ruling, not by this criterion.

Deliberately ignored: issue age and filing order Β· the priority label alone (severity, re-ranked here on reachability and leak direction) Β· issue number Β· how cheap or well-understood a fix looks Β· track-sharing (#3283 and #3284 sit on WIRE-TRUTH, #3325 on ATTACHMENT-TRUTH β€” sharing does not move an item here).

Standing rulings

History / forensics

What already merged
  • PR #3134 (merged 2026-08-07) gated the FindBridges profile before it reaches the LLM prompt β€” the profile half of #3117/#3135.
  • PR #3290 (merged 2026-08-14) gated the semantic-connections serving reads on the audience axis β€” one matcher of the four in #3141/#3284.
  • Issue #2906 (closed) β€” the Q&A pairing splice; it closes the question side, not the answer side (#3118).
The Regina audience census (#3128, #3285)
  • 155 question groups span more than one distinct contributing message; 232 contributing messages' audiences are discarded.
  • 25 pairs are under-gated (ignored carrier strictly tighter); 82 pairs are incomparable β€” every one on the Regina tenant.
  • Over-suppression: 12 Regina role edges point above tier 0, so an entitled viewer resolves to no audience identity and sees nothing.
The NULL-provenance census (#2905, #3118)
  • Content nodes with no message anchor (audience never consulted): answer 100% (1,905/1,905 on 08-01), offer 43.1% (1,311/3,045), question 11.2%, problem 6.8%, need 4.6%, document 0.
  • All 2,023 answer nodes have source_message_id NULL (measured 08-07) β€” full verbatim conversation text that always takes the structural passthrough.