Architecture¶
The Agent Eligibility Gateway is one layer in a two-path enforcement composition. A request reaches an agent through exactly one of two paths, and the governing controls differ by path. Understanding which path applies to a given agent is required before deploying — or deciding not to deploy — this gateway.
Why there are two paths¶
Custom middleware can only sit in the request path of a channel that the organization fronts itself. First-party agent surfaces — Microsoft Teams, Microsoft 365 Copilot, and SharePoint — do not expose a middleware insertion point, so a request to an agent on those surfaces cannot traverse an APIM gateway. As a result:
- Owned channels (custom web chat over Direct Line / Web Chat, and Direct Line API integrations) can be fronted by this gateway, which renders a hard allow/deny at request time.
- First-party surfaces are governed by native Managed Environment sharing plus detective telemetry-drift detection, which apply after the fact and with latency.
The two paths are complementary. The gateway does not replace native controls, and native controls do not provide the request-time allow/deny that the gateway provides on owned channels.
Path A — Owned-channel gateway (this solution)¶
Client app (owned custom-web / Direct Line channel)
|
v
Microsoft Entra ID sign-in
| precondition: bot.authenticationmode = Integrated or Custom Azure Active Directory
| bot.authenticationtrigger = Always (require users to sign in = ON)
| bearer token
v
Agent Eligibility Gateway (Azure API Management <inbound>)
|
| 1. validate-jwt tenant OpenID config; audience; issuer; tid claim --> 401
| 2. audience / Viewers caller must be in the agent's Viewers group --> 403
| 3. governance-store read managed identity -> fsi_caicompliancestate + fsi_cbgentitlementmaterialized
| (fsi_risklevel; fsi_decision / fsi_pathway)
| 4. entitlement contract switch-on-pathway (see below) --> 403 on deny
| 5. decision telemetry one structured record per request
|
+--> allow --> Agent endpoint (Copilot Studio / custom agent backend)
|
+--> telemetry sink (Event Hub -> Function / Stream Analytics)
|
v
fsi_aegdecisionlog (per-decision audit, control 3.8)
Decision model (switch-on-pathway). Eligibility is evaluated only inside a metered pathway:
| Condition | Decision | HTTP |
|---|---|---|
| Token invalid / expired / wrong tenant | Deny | 401 |
| Caller not in the agent's audience / Viewers group | Deny | 403 |
| Agent non-compliant in the governance store | Deny | 403 |
Pathway metered and caller in no eligible cohort |
Deny | 403 |
Pathway none |
Allow | 200 |
| Pathway unmapped / unknown | Allow (anomaly) | 200 |
Pathway mcp-cs / mcp-agentbuilder / api-direct |
Allow | 200 |
The deny on a metered pathway with no eligible cohort, and the allow on a none pathway, together implement the corrected entitlement contract: billing eligibility gates a user only where a metered pathway makes eligibility meaningful. An unmapped pathway fails open with an anomaly flag so that a classification defect is surfaced for investigation rather than translated into a user-facing denial.
Failure posture. Token problems fail closed at validate-jwt (401). The audience gate fails closed (403) — including the groups-overage case where the groups claim is omitted for callers in many groups (see apim-gateway-setup.md for the managed-identity Graph fallback). Compliance fails closed: a missing governance-store row or a store outage is treated as non-compliant. Cohort lookup on a metered pathway fails closed (no eligible cohort found means deny). These are conservative defaults; confirm them against the customer's risk posture during ratification.
Path B — First-party native controls + telemetry drift¶
Client (Microsoft Teams / Microsoft 365 Copilot / SharePoint)
|
v
First-party agent surface
| no middleware insertion point -> the gateway is not in this path
v
Native Managed Environment controls
| - agent sharing (bot-* extended settings) governs who the agent is shared with
| - audience / Viewers security groups govern chat access
| - enforcement applies with roughly an hour of latency
v
Telemetry (Microsoft Purview audit, Dataverse, App Insights)
|
v
Detective telemetry-drift solutions in this repository
(for example agent-sharing-access-restriction-detector, scope-drift-monitor)
-> raise findings after the fact; no request-time allow/deny
On this path there is no hard allow/deny at request time. Access is shaped by native sharing and audience configuration, and deviations are caught after the fact by the detective solutions that monitor telemetry. The roughly one-hour enforcement latency of Managed Environment sharing is a property of the native control, not of this gateway.
Control-layer summary¶
| Layer | Scope | Timing | What it does | Owner |
|---|---|---|---|---|
| Agent Eligibility Gateway (this solution) | Owned custom-web / Direct Line channels | Request time (synchronous) | Hard allow/deny: token validation, audience gate, compliance + entitlement contract, per-decision audit | This solution |
Managed Environment agent sharing (bot-* extended settings) |
First-party surfaces and platform-wide sharing | ~1 hour enforcement latency | Governs who an agent is shared with and the audience that can chat | Power Platform native |
| Telemetry-drift detection | First-party surfaces (and owned, as defence in depth) | After the fact | Detects sharing / scope / access drift from telemetry and raises findings | Sibling detective solutions |
The gateway is the only layer in this composition that provides a synchronous, request-time allow/deny, and it does so only on owned channels. Everywhere else, governance is the combination of native sharing controls and detective telemetry-drift monitoring. This boundary is the reason the gateway is optional and conditional: a tenant whose agents run only on first-party surfaces has no owned-channel request path for the gateway to sit in.
Where the gateway reads its inputs¶
The gateway is a reader of governance state, never a writer of it:
| Input | Source table | Owning solution | Logical names (reconciled 2026-06-09) |
|---|---|---|---|
| Agent compliance posture | fsi_caicompliancestate |
copilot-agent-inventory |
fsi_risklevel (keyed on fsi_agentid) |
| Precomputed entitlement decision | fsi_cbgentitlementmaterialized |
copilot-billing-governance |
fsi_decision, fsi_pathway, fsi_decisionreason (keyed on fsi_agentid + fsi_userupn) |
| Audience / Viewers membership | Microsoft Entra ID groups claim |
Tenant identity | groups claim on the validated token |
The optional fsi_aegdecisionlog table is the gateway's only output to Dataverse, and it is written asynchronously by the telemetry sink, not on the request path. The column logical names for fsi_copilotagent and fsi_cbgentitlementmaterialized are assumptions to confirm against the sibling solutions' schema scripts before production use.
Deny-reason vocabulary mapping (CBG → AEG)¶
The billing engine and the gateway use different deny-reason vocabularies. The materialized decision the gateway reads carries a CBG block reason (fsi_decisionreason, option set fsi_cbg_blockreason), but the gateway's own audit column is fsi_denyreason (option set fsi_aeg_denyreason), which is coarser. The telemetry sink applies the mapping below when it lands a record in fsi_aegdecisionlog; the verbatim CBG reason is retained in fsi_rawcontext, so the precise CBG reason remains available for audit.
CBG fsi_cbg_blockreason |
Resulting CBG fsi_cbg_decision |
AEG fsi_aeg_denyreason |
|---|---|---|
| No eligible cohort (100000000) | Block | NotInEligibleCohort (100000004) |
| Missing license (100000001) | Block | NotInEligibleCohort (100000004) |
| Zero-rating unresolved - fail-closed (100000002) | Fail-closed - Zero-rating Unresolved | NotInEligibleCohort (100000004) |
| Not in credit scope (100000003) | Block | NotInEligibleCohort (100000004) |
| Policy cap exceeded (100000004) | Block | NotInEligibleCohort (100000004) |
| Unmapped pathway (100000005) | Fail-open - Anomaly | None (100000000) — allow + anomaly, not a deny |
The gateway's own gates set AEG-native deny reasons directly, with no CBG input: the audience gate sets OutOfPolicyAudience and the compliance gate sets AgentNonCompliant. Token failures are handled earlier in aeg-validate-jwt and surface as 401s (JwtValidationFailed / MissingRequiredClaim), not as gateway 403s. The compliance read fails closed, so a missing compliance row or a transient governance-store outage currently resolves to AgentNonCompliant; the GovernanceStoreUnavailable value in the AEG option set is reserved for a sink that chooses to distinguish a store outage from a genuine non-compliance. Because the AEG vocabulary collapses every billing-side denial into NotInEligibleCohort, the CBG → AEG mapping is one-directional and lossy by design — consult fsi_rawcontext for the precise CBG reason. Confirm the AEG option-set values against dataverse-schema.md before relying on them.
Reference gateway vs. customer deployment¶
Architecturally, the reference gateway and a customer deployment are the same APIM policy composition pointed at different backends:
- The reference gateway runs in a non-production environment, points at representative test agents, and validates the entitlement contract end-to-end (allow/deny outcomes, anomaly fail-open, decision logging) before any production rollout is considered.
- A customer deployment points the same policy fragments at a production owned-channel backend. It is optional and applies only where owned custom-web / Direct Line channels exist.
Keeping the two separate lets the entitlement contract be validated as a design artifact independently of whether — and where — a customer chooses to deploy the gateway in production.