Member Junction
    Preparing search index...

    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):

    1. Agent.SearchScopeAccess === 'None' → reject (explicit agent-side deny). 1b. Agent.SearchScopeAccess === 'Assigned' and this scope is not in its list → reject. 1c/1d. The SAME two rules for a SKILL principal, deliberately identical in shape. 1e. A supplied SKILL must be ACTIVATABLE by the caller → otherwise reject. This one surprises people, so it is worth stating here rather than only at the code. A skill is judged wherever it is NAMED, not only where it grants, because it steers the bound through a surface this verdict never sees: SearchParams.AISkillID binds into Principals.SkillID, and for a restricts: true dimension 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.
    2. Direct grant: a SearchScopePermission row with UserID = user.ID and SearchScopeID = scope.ID. PermissionLevel = 'None' is an explicit deny that short-circuits and rejects regardless of role grants.
    3. Role grants: SearchScopePermission rows where RoleID is in the user's UserRoles. The highest non-None level wins. None entries are ignored at the role level (see comment below).
    4. Agent.SearchScopeAccess === 'All' → allow at Search level (lets trusted agents act across all scopes when no user-side grant exists), but ONLY if the caller may actually run that agent. The agent is judged HERE and not at supply because elsewhere AIAgentID is attribution — pre-execution RAG threads it purely for SearchExecutionLog — and gating attribution turns an analytics field into an outage. A DENIAL here rejects (for the message — 4b would refuse it too). A merely unevaluable agent falls through, and is then rejected by 4b if an 'All' skill follows, or by step 5 if not. Either way the outcome is a rejection; the distinction survives in the message. 4b. Skill.SearchScopeAccess === 'All' → allow at Search level, but ONLY with the agent positively confirmed. Step 1e is not sufficient here: it vouches for the SKILL, and GetSkillsForAgent reads SKILL permissions, which say nothing about whether this caller may run the agent the skill would activate on. Both a denial and an unevaluable agent refuse.
    5. No grant → reject, naming the unwieldable 'All' agent if that was the only candidate.

    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.

    Hierarchy (View Summary)

    Index

    Constructors

    Methods

    • May 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.

      Parameters

      Returns Promise<
          | { ok: true }
          | { kind: "unavailable" | "denied"; ok: false; reason: string },
      >

    • Checks 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.

      Parameters

      • agentID: string
      • searchScopeID: string
      • contextUser: UserInfo

      Returns Promise<boolean>

    • Whether 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.

      Parameters

      • skillID: string
      • searchScopeID: string
      • contextUser: UserInfo

      Returns Promise<boolean>

    • Could 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.

      Parameters

      Returns Promise<
          | { ok: true }
          | { kind: "unavailable" | "denied"; ok: false; reason: string },
      >