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.