Straza / two-tier catalog

session
policyFilter
The two-tier MCP catalog Candidate tools pass two gates. Tier 1, the shared role catalog, keeps only tools bound to the session's roles (inventory intersect exposure intersect bindings); a tool with no binding is never a candidate, and the built-in native straza tools join as candidates for every role with no binding at all. Tier 2, the per-session overlay, probes the policy engine once per tier-1 tool with the full subject: a denied app tool is hidden when policyFilter is on, a denied native tool is always hidden, and an approve-gated tool stays visible with an injected justification field. The same Tier 1 serves both sessions; only the Tier 2 overlay differs. The final tools/list is Tier 1 minus the hidden set, paginated behind an opaque self-validating cursor. subject alice · roles: developer · userType: human · attestation: verified CANDIDATE TOOLS TIER 1 · shared ∩ inventory ∩ exposure ∩ bindings TIER 2 · per session probe engine · full subject session tools/list tier-1 minus hidden · opaque cursor DEFAULT-DENY: no binding ⇒ never a candidate · a native tool is visible only when policy authorizes it TIER 1 is identical for both sessions (same roles, cached + shared); only the TIER 2 overlay differs by user / typology / attestation tools/list paginates behind base64url(1\0digest\0offset); any catalog change fails the cursor with -32602 and the client re-lists
Switch session (A alice / B bob) and toggle policyFilter to watch the same shared Tier 1 produce a different per-session tools/list. Hover a tool row for its reasoning. Native straza tools carry no binding, so policy is their only visibility gate.
What this shows

A gateway tools/list is built in two tiers. Tier 1 is the shared role catalog: per app, cached upstream inventory intersected with manifest exposure and the union of the session roles' tool bindings. A tool with no binding is never a candidate (default-deny), so it cannot appear no matter what. The built-in native straza app (approval_request and friends) has no apps-table row and no binding, so it is appended as a candidate for every role. One Tier 1 is computed per role set and shared across every session that holds those roles. Tier 2 is the per-session overlay: it probes the policy engine once per Tier 1 tool with the session's full subject (user, roles, identity typology, attestation), which is what lets user-scoped and typology-scoped rules take effect. A non-allow verdict hides the tool when apps.catalog.policyFilter is on; a denied native tool is always hidden, since policy authorization is its only visibility gate; an approve-gated tool stays visible with an injected _straza_justification field. The final list is Tier 1 minus the hidden set, with approve-gated schema swaps, paginated behind an opaque self-validating cursor that fails closed on any catalog change. Because the two sessions here hold the same role, they share one Tier 1; only their Tier 2 overlays differ.