ProtectedagentMay this user run this agent? Asked at the WIDENING FALLBACKS — the agent's own
SearchScopeAccess='All' arm (step 4) and, because a skill widens through the agent it would
activate on, the skill's 'All' arm too (step 4b). Not only the agent's own arm: a non-'All'
agent still reaches this check when the SKILL is 'All'. Agent permissions are open by default
(no rows means anyone may run it), so without this an id a caller merely NAMED could grant
Search.
Deliberately NOT asked at the point an AIAgentID is supplied: pre-execution RAG threads it
purely so SearchExecutionLog can attribute the search, and gating that turns an analytics
field into a retrieval outage.
A stale metadata cache is reported distinctly. GetUserAgentPermissions throws when the agent
is absent from AIEngine.Instance.Agents and fails closed to all-false, so an agent created
after the cache loaded would otherwise read as "not permitted" — a metadata-load problem
wearing an authorization message.
ProtectedapplicableKeep only grants that are in force at this moment and apply to this tenant.
Both dimensions are additive: a row that leaves StartAt/EndAt/PrimaryScopeRecordID
NULL is always in force and applies to every tenant, which is exactly how every row
behaved before those columns existed.
ProtectedisA grant with a NULL tenant applies everywhere. A tenant-scoped grant applies ONLY to that tenant — and, notably, does not apply when the search supplies no tenant at all, because "this grant is for org A" cannot be honoured by an untenanted search.
ProtectedisA grant with no window is always in force; otherwise now must fall inside it.
ProtectedisChecks whether the given scope is in the agent's assigned-scope list via __mj.AIAgentSearchScope. Used to enforce the SearchScopeAccess='Assigned' deny-list rule. Returns true when at least one matching row exists with Status='Active'; false otherwise.
ProtectedisWhether the scope is in the skill's assigned-scope list via __mj.AISkillSearchScope.
Mirrors isScopeAssignedToAgent, including honouring Status and the optional time window
(which the agent table also has). Fails closed on an unreadable table.
ProtectedloadLoads all SearchScopePermission rows scoped to the given SearchScope. Caller-supplied ContextUser ensures the RunView runs under the same identity the rest of the request is using.
Resolves the effective permission. All UUID comparisons go through UUIDsEqual to remain case-insensitive across SQL Server / PostgreSQL.
ProtectedskillCould this caller actually activate this skill on this agent? Asked wherever a skill is NAMED, because a skill steers the bound from a surface the permission verdict never sees — see step 1e.
DOES NOT CHECK THE AGENT, AND THAT IS NOT SUFFICIENT ON ITS OWN. GetSkillsForAgent is
agent-accepted ∩ agent-granted ∩ Active ∩ user-runnable-ON-THE-SKILL — read it
(BaseAIEngine.GetSkillsForAgent): its permission filter is AISkillPermissionHelper, the
user's rights on the SKILL. It never consults AIAgentPermission. So a user who may not run
the agent still gets a NON-empty list here, because skill permissions are open by default.
The agent is therefore judged separately, at the fallbacks (steps 4 and 4b), NOT here. That keeps an agent-side cache problem from refusing a user whose own grant covers the scope.
It does NOT make this method exempt from the same hazard: step 1e runs BEFORE the grant steps, so a skill that cannot be confirmed refuses such a user too. That is deliberate — an unconfirmed skill must not steer the bound — but it is a real availability cost, which is why the cache case is separated from a denial below and reported as what it is.
Resolves the effective SearchScope permission for a (user, scope, agent, skill) tuple — plus the caller's tenant (
PrimaryScopeRecordID), which narrows tenant-scoped grants.Resolution order (later steps only run if the earlier did not produce a definitive answer):
restricts: truedimension the expansion query's output IS the enforced bound. Judging it only at step 4b would let a user holding their own grant (steps 2/3) name any skill and widen with it. The AGENT is deliberately NOT judged here — see step 4.The user-direct-None rule (step 2) is intentional: an admin who explicitly denies a user on a scope should not have that decision overridden by a role membership the user happens to also hold. Role-level None entries are not authoritative because they are usually authored as a placeholder (e.g., to make a row exist before granting it later) and would create surprising lockouts when a user joins a role.