Entitlement Contract — Switch-on-Pathway¶
Status: Preview (v0.1.0-preview). This is the corrected entitlement contract for Copilot Billing Governance. It replaces an earlier deny-by-default design (
ELSE → block) that would have blocked the agent majority. See Assumptions.
This document is the single source of truth for how the entitlement engine
decides whether a given user may invoke a given agent under Copilot consumption
billing. It is implemented as a decision tree plus pseudocode; the runnable
skeleton lives in scripts/Invoke-EntitlementEvaluation.ps1.
2026-10-16 verification boundary: this contract describes the shipped preview implementation, not Microsoft's complete licensing or access rules. The
mcp-cs/mcp-agentbuilderlicense-only gates can differ from Microsoft-supported metered access; Work IQ API requires its own spending-policy evaluation, which this engine does not perform. Keep analysis monitor-only and reconcile any "blocked" conclusion with the applicable Microsoft service. Sources and unresolved implementation/schema ownership are recorded in the README.
1. Design principle: switch on pathway, not deny-by-default¶
The load-bearing correction is that the engine classifies the agent's
consumption pathway first, then applies pathway-specific eligibility. A single
global ELSE → block would deny the large population of agents that consume no
metered Copilot features. Instead:
- There is an explicit
none → ALLOW (eligibility N/A)arm that unblocks the agent majority. - A bounded
ELSE → blockexists only inside a metered pathway, where a user who is in no eligible cohort is correctly blocked. - An unmapped pathway fails open with an anomaly — a detection defect must not deny a user. The anomaly is recorded for follow-up rather than silently allowed.
This contract supports compliance with Control 3.5 (Cost Allocation and Budget Tracking) and contributes to Control 1.18 (Application-Level Authorization and RBAC) and Control 1.14 (Data Minimization and Agent Scope Control). It does not, on its own, satisfy any regulation; organizations should verify their configuration meets their specific obligations.
2. Inputs¶
| Input | Source | Used for |
|---|---|---|
createdIn |
Azure Resource Graph (PowerPlatformResources table) |
Pathway classification (which environment/tooling built the agent) |
configuredTier |
Work IQ usage detection (work-iq-usage-detection solution) |
Pathway classification + whether the agent uses metered features |
| Tenant-grounding signal | Agent configuration (botcomponent) | Feature cost estimate; apply the current billing table's licensed-user inclusion and exceptions rather than inferring a universal tenant-grounding surcharge |
| User Copilot license | Microsoft Graph license read | Per-pathway eligibility |
| Group membership | Entra security groups in the admission-gated registry (fsi_cbgapprovedgrouppolicy) |
Eligible-cohort / credit-scope checks |
| Credit / PAYG policy state | fsi_cbgcreditpolicy, fsi_cbgbillingpolicy |
Surface (Chat vs SharePoint) and policy availability |
Assumption (dependency):
createdInandconfiguredTierare produced by the sibling previewscopilot-agent-inventoryandwork-iq-usage-detection, both catalog-registered. When live inputs are unavailable, use fixtures (see the script's-InputPathparameter).
2.1 Per-user input resolver (Get-CopilotEntitlement.ps1)¶
The engine consumes six per-user fields under each agent's intendedUsers[]: upn,
hasCopilotLicense, inApiAudienceGroup, inCreditScopeGroup, inEligibleCohort,
and surfaceZeroRated. During development these come from fixtures; in production they
are produced from real tenant data by
scripts/Get-CopilotEntitlement.ps1. The
resolver takes a set of UPNs (or an agents skeleton whose agents carry intendedUpns)
and returns / persists the engine-ready document plus a Find-No-Filter (FNF)
"blocked from a gated capability" lens.
hasCopilotLicense— by literal service-plan GUID, never by name. The resolver readsGET /users/{id}/licenseDetails(which includes transitive, group-assigned licenses) and setshasCopilotLicense = trueonly when the user holds at least oneservicePlans[]entry whoseservicePlanIdis in the paid Microsoft 365 Copilot allowlist (eight GUIDs) and whoseprovisioningStatusisSuccess. Detection is by the literal GUID allowlist — never aCOPILOTsubstring orservicePlanNameregex.- License-read authentication. The current
Graph licenseDetails API
supports delegated
LicenseAssignment.Read.All(or a supported higher delegated permission) and a supported signed-in Entra role, not application permissions. A managed identity withUser.Read.Allis not a supported resolver license-read path. Use delegated diagnostic runs until an unattended replacement is validated. - DENY trap. Confusable plans are explicitly excluded and can never grant
entitlement:
Bing_Chat_Enterprise(the free Copilot Chat plan bundled in E5 — the key false-positive trap), Microsoft Sales Copilot, and Viva Sales. An E5 user whose only Copilot-looking plan isBing_Chat_Enterpriseis therefore not licensed. - Tenant SKU reconciliation. The resolver does not hard-code SKU display names. It builds a
tenant SKU dictionary from
GET /subscribedSkusand resolves entitlement from the service-plan allowlist, so any local SKU carrying an allowlisted Copilot plan is classified by the implementation's allowlist regardless of its display name. The allowlist is an implementation heuristic, not proof that every future SKU carrying a shared plan grants every capability. - PAYG / credit coverage →
inCreditScopeGroup. There is no Graph endpoint for billing-policy membership. The resolver reads pay-as-you-go / credit policy scope and maps coverage for the gated capability to the engine'sinCreditScopeGroupinput: a policy scoped to All Users covers every tenant user for its surface and collapses the blocked set to zero for that capability (surfaced as a loud warning so "0 blocked" is not misread as "all entitled"); a group-scoped policy covers the group's transitive members (GET /groups/{id}/transitiveMembers); coverage is evaluated per capability (PAYG today covers Chat / SharePoint, not every feature), so a SharePoint-only policy does not cover a Chat gate. - Fail-CLOSED on policy-shape uncertainty. A false "covered" silently flips unlicensed
in-scope users to not-blocked and under-reports the people who actually lack access, so
the resolver grants PAYG coverage only from a policy it can fully parse. A policy is
treated as NOT covering — routed to a
needsManualReview[]list (distinct fromappliedPolicies) withcoverageUncertainset — when its connection state is undetermined (noconnected/isConnected/statussignal;connecteddefaults to false and a positive signal is required, because a billing policy not connected to a service entitles no one), its capability surface is unrecognized (none of the known capabilities / services / connectedServices / serviceTypes / spendScope / surfaceScope fields, so coverage of the gated capability cannot be confirmed), or its scope cannot be resolved. Affected unlicensed users stay reported as blocked pending manual verification against the Microsoft 365 admin center rather than being granted on an unparseable policy. - FNF "blocked" lens. Independently of the engine decision, the resolver computes
isBlocked ⇔ (no paid Copilot service plan) AND (not covered by an applicable PAYG / credit policy for the gated capability)— always joining license and PAYG.Coverage-limit caveat (F5):
isBlockedis a resolver-level pre-filter, not the authoritative engine decision. For themcp-agentbuilderpathway (the typical path for Agent Builder People-capable declarative agents), PAYG / credit policy coverage does not substitute for a Microsoft 365 Copilot license — the engine blocks an unlicensed user regardless of PAYG coverage (see §4). A user flaggedisBlocked = falsebecause they hold PAYG coverage but no license may still receive an engineBlockon this pathway. Always verify against engine decisions (fsi_decision = Block) rather than relying onisBlockedalone for pathway-specific conclusions. The FNF People-Sweep report uses engine decisions for itsblockedUserCount, so the report output is authoritative; the lens field is a summary aid only. - Accuracy-first (fail-open only on transient read error). Misclassifying a licensed user as "blocked" is a serious, customer-facing error, so a user whose Graph license read fails (a transient per-user I/O error) is recorded as unresolved (never "blocked") and excluded from the engine input for manual review, rather than asserted blocked on incomplete data. Fail-open is reserved for these transient per-user read errors; policy-shape uncertainty fails closed (previous bullet).
Assumption (unproven schema). The Power Platform billing-policy REST shape (
https://api.powerplatform.com/licensing/billingPolicies?api-version=2024-10-01) is not proven to provide complete Microsoft 365 Chat credit-policy scope, so the resolver prefers an explicit-BillingPolicyInputPath/-BillingPolicy(for example, the normalized output ofGet-BillingPolicyInventory.ps1) and treats the live read as best-effort. The PAYG →inCreditScopeGroupmapping is a deliberate design choice; confirm coverage against the Microsoft 365 admin center.
3. Pathway classification¶
ClassifyPathway(agent) maps an agent to one option of the fsi_cbg_pathway global
option set. configuredTier is the authoritative signal and is evaluated first;
createdIn is consulted only as a last-resort fallback when configuredTier is
empty or unrecognized. This ordering is load-bearing: it keeps a Copilot-Studio
createdIn from overriding an authoritative non-metered tier and forcing the agent
majority into the stricter (license-gated) mcp-cs arm.
The sibling work-iq-usage-detection classifier emits one of the following
configuredTier values (compared lowercased): NotConfigured,
NativeMcpCopilotStudio, NativeApiDirect, Adjacent.
3a. Authoritative configuredTier mapping (evaluated first)¶
configuredTier (Work IQ) |
Pathway (fsi_cbg_pathway) |
|---|---|
NativeMcpCopilotStudio |
mcp-cs |
NativeApiDirect |
api-direct |
NotConfigured, Adjacent (and none / classic / non-metered synonyms) |
none |
metered / generative / grounded / agent-action / premium |
metered |
NotConfigured and Adjacent are non-metered → none → ALLOW (eligibility N/A),
which unblocks the agent majority.
3b. createdIn fallback (only when configuredTier is empty / unrecognized)¶
createdIn signal |
Pathway (fsi_cbg_pathway) |
|---|---|
| Copilot Studio environment | mcp-cs |
| Agent Builder | mcp-agentbuilder |
| API / declarative / direct-line / custom | api-direct |
| Missing or contradictory signals | unmapped (fail-open with anomaly) |
The resulting pathway carries the meaning below regardless of which signal produced it:
Pathway (fsi_cbg_pathway) |
Meaning |
|---|---|
none |
Consumes no metered Copilot features (classic / no generative, no grounding) |
mcp-cs |
Copilot Studio–built MCP agent |
mcp-agentbuilder |
Microsoft 365 Copilot Agent Builder MCP agent |
api-direct |
Direct API / declarative agent on an owned channel |
metered |
Agent on a metered consumption path not covered above |
unmapped |
Classification could not be resolved (missing or contradictory signals) |
4. Decision tree¶
EvaluateEntitlement(agent, user):
pathway ← ClassifyPathway(agent) # Work IQ configuredTier first; createdIn fallback
switch pathway:
case none: # ── the agent majority ──
return ALLOW reason = "eligibility N/A"
case mcp-agentbuilder:
if user.hasCopilotLicense: return ALLOW
else: return BLOCK reason = "Missing license"
case api-direct:
if user ∈ apiAudienceGroup: return ALLOW
else: return BLOCK reason = "No eligible cohort"
case mcp-cs: # shipped default; validate inclusion exceptions
if not user.hasCopilotLicense: return BLOCK reason = "Missing license"
if ZeroRatingResolved AND zeroRated(agent.surface):
return ALLOW
if user ∈ creditScopeGroup: return ALLOW
return FAIL_CLOSED reason = "Zero-rating unresolved"
case metered:
if user ∈ eligibleCohort(agent): return ALLOW
else: return BLOCK reason = "No eligible cohort"
# ← the ONLY unbounded-population ELSE,
# and it is bounded to a metered pathway
default: # unmapped
return FAIL_OPEN_ANOMALY reason = "Unmapped pathway"
# a detection defect must not deny a user; the anomaly is recorded
Decisions map to the fsi_cbg_decision option set: Allow, Block,
Allow - Eligibility N/A, Fail-open - Anomaly, Fail-closed - Zero-rating Unresolved.
Block reasons map to fsi_cbg_blockreason.
5. Zero-rating modifier — shipped default and current inclusion limits¶
The engine still defaults ZeroRatingResolved to true. The current
Copilot Studio billing table
includes employee-facing usage under the authenticated licensed user's identity,
subject to fair-use limits. That is not a universal entitlement for all services:
- Agent-flow inclusion requires When an agent calls the flow; other triggers consume credits at the standard rate.
- Computer-Using Agents are excluded from the Microsoft Copilot user-license inclusion.
- Work IQ API consumption by custom apps/agents uses Copilot Credits and
separate spending-policy access. Do not apply the generic
mcp-cszero-rating assumption to it. See Work IQ API licensing. - The table explicitly lists generative answers and tenant graph grounding as No charge for the qualifying licensed-user case. Its general generative-answer footnote is not evidence of a blanket tenant-grounding surcharge on that case.
The mcp-cs arm requires a license AND (the surface is zero-rated OR the user
is in credit scope):
mcp-cs eligible ⇔ hasCopilotLicense AND ( (ZeroRatingResolved AND zeroRated(surface))
OR user ∈ creditScopeGroup )
ZeroRatingResolvedis carried on eachfsi_cbgentitlementrow and defaults totruein the implementation: a Copilot-licensed user on a qualifying surface under their own identity (surfaceZeroRated = true) is Allowed — the license is sufficient, no credit scope required.- Implementation limitation: the
mcp-csarm blocks an unlicensed user even when in credit scope; it does not implement all Microsoft-supported metered access scenarios. For licensed users not marked zero-rated, the engine requires credit scope. Do not present either rule as a universal Microsoft product requirement. surfaceZeroRatedis supplied data, not a capability-aware determination. The resolver'struedefault does not check the exceptions above. Confirm the actual operation, identity, service, and trigger before setting it.- Set
ZeroRatingResolved = false(script:-ZeroRatingResolved:$false) to revert to the conservative fail-closed posture; licensedmcp-csusers not in credit scope then resolve toFail-closed - Zero-rating Unresolvedrather than a silent allow.
Disabling the flag is a conservative diagnostic fallback, not a substitute for evaluating service-specific spending policies. Schema descriptions and script help still referencing the June Guide interpretation need an owning-specialist update.
6. Materialized decision cache (fsi_cbgentitlementmaterialized)¶
Recomputing the full decision tree per read does not scale to large
(agent × user) populations. Each decision is materialized to
fsi_cbgentitlementmaterialized with a time-to-live (fsi_ttlexpiresat).
Reads consult the cache; expired or missing entries are recomputed. The cache
stores the pathway, decision, block reason, spend scope, and source policy so a
decision is auditable without replaying inputs.
Assumption: TTL duration is intentionally left as a deployment parameter (
-CacheTtlMinutes, default documented in the script). "Decide late": tune it from observed input-change cadence rather than pre-committing a value here.
7. Coverage-gap analysis (the load-bearing pre-enforcement deliverable)¶
Before any enforcement, the solution runs a per-agent coverage-gap aggregate
in monitor-only mode. For each agent it records one fsi_cbgcoveragegap row:
| Field (logical name) | Meaning |
|---|---|
fsi_agentid |
Analyzed agent |
fsi_pathway |
Classified pathway |
fsi_eligibleusers |
Count of users eligible under current policies |
fsi_blockeduserscount |
Count of intended users who would be blocked |
fsi_blockedsampleupns |
Capped JSON sample of blocked UPNs (bounded to avoid row blow-up) |
fsi_blockreasonsummary |
Dominant block reason across the cohort |
fsi_spendscope |
Surface-aware scope (Chat vs SharePoint) |
fsi_groupsizepartition |
Total intended-audience size (partitions large groups above threshold T) |
fsi_monitoronly |
true first — gap rows take no enforcement action |
fsi_retainuntil |
Retention horizon for the aggregate |
Aggregating per agent (not per agent × user) keeps the output bounded; a naive per-pair materialization of every blocked user would produce a 10⁶–10⁷-row blow-up. The capped UPN sample preserves investigability without the row explosion.
7.1 Per-feature credit rates (coverage-gap cost estimate)¶
Coverage-gap spend is estimated from Microsoft-published per-feature Copilot credit rates. These are reference constants (not a Dataverse table); verify current values against Microsoft licensing documentation before relying on them.
| Feature | Credits |
|---|---|
| Classic answer | 1 |
| Generative answer | 2 |
| Agent action | 5 |
| Tenant-graph grounding | 10 |
| Agent flow | 13 per 100 flow actions |
| Text/generative AI tools | Basic / standard / premium: 1 / 15 / 100 per 10 responses; token meters 0.1 / 1.5 / 10 per 1,000 tokens |
| Content processing tools | 8 per page |
| Voice | 10 / 35 / 75 per minute (tiered) |
Source: current billing rates. Reasoning models add the premium AI-tools token meter to the feature rate; the feature table alone is not a complete estimate. Rates above apply to the standard harness, exclude bring-your-own models, and have the licensed-user conditions in §5.
The prepaid pack is 25,000 credits per month, non-rolling (unused credits do not carry over). PAYG consumption is metered against an Azure subscription with budget alerts only — not a hard-stop.
Language caveat: these figures support cost estimation; they do not produce a billing-accurate invoice. Organizations should reconcile against the Microsoft 365 admin center and Azure cost reporting.
8. Two policy objects and three configurations¶
PAYG billing policy (fsi_cbgbillingpolicy) |
Credit policy (fsi_cbgcreditpolicy) |
|
|---|---|---|
| Backing | Azure subscription | Prepaid (no Azure subscription) |
| Tenant ceiling | 50 | 10 |
| Setup | Two-step add → connect | Standalone |
| Spend control | Budget alerts (not a hard-stop) | Standalone hard-stop |
| Surfaces | Connected Chat / SharePoint agent service, not all Copilot services | Chat-only today; SharePoint agents use PAYG |
Three supported configurations: credit-only, credit + PAYG, and PAYG-only.
The fsi_spendscope field carries the surface (Chat vs SharePoint) so the engine and
coverage-gap remain surface-aware.
The 50 / 10 ceilings and three configurations are confirmed by PAYG setup and capacity packs. Create the policy, connect PAYG to its intended service, and allocate prepaid credits to the Microsoft Copilot Chat environment before asserting coverage. Standalone exhaustion depends on available capacity/overage settings, not a CBG cap write. Work IQ API spending policies in Copilot > Cost management are separate from these two objects and are not yet evaluated by CBG.
9. Proposed framework control 2.27 (cross-repo follow-up)¶
This is a proposal for the companion framework repo
judeper/FSI-AgentGov, not a control implemented by this manifest.manifest.yamlintentionally lists only existing framework controls (3.5,1.18,1.14).
Proposed Control 2.27 — Consumption-Entitlement Governance (Pillar 2, Management). Governs who is entitled to consume metered agent capabilities under which billing or credit policy, distinct from identity governance and authorization.
Why a new control rather than extending an existing one (rule-out, per the owl review):
- Not 2.26 (Entra Agent ID — Identity Governance for Agents). 2.26 governs the agent's identity lifecycle. Consumption entitlement governs a user's permission to spend on an agent's metered features — a billing/authorization archetype, not an identity-provisioning archetype. Folding it into 2.26 would conflate two different control objectives.
- Not 1.18 (Application-Level Authorization and RBAC). 1.18 governs functional authorization (can this principal call this capability). Consumption entitlement adds a cost/billing dimension (is this principal entitled to incur metered spend, under which policy, on which surface). 1.18 is a necessary input but does not express the billing-scope or zero-rating semantics.
2.27 would pair with 3.5 (Cost Allocation and Budget Tracking) — 3.5 is the Reporting-pillar view; 2.27 is the Management-pillar control that governs the entitlement decisions 3.5 reports on. Placement (Management vs Reporting) is open for ratification with the framework owner.
Assumptions and build-time verifications¶
- Deny-by-default replaced. The brief's global
ELSE → blockis replaced by switch-on-pathway with an explicitnone → ALLOWarm and a bounded metered-onlyELSE. Assumption: the agent majority isnone-pathway. - Zero-rating is conditional. The shipped
fsi_zeroratingresolveddefault istrue; validate current licensed-user inclusion, trigger requirements, CUA exclusions, and Work IQ billing separately (§5). This is not a production entitlement attestation. createdIn/configuredTierare upstream. Sourced fromcopilot-agent-inventory(ARGPowerPlatformResources) andwork-iq-usage-detection. Work IQ API GA / consumption pricing started June 16, 2026, not all Copilot Credits billing.- Write integration unproven, native UI available. Configure native agent caps at Licensing > Copilot Studio > Summary > Manage Agents. CBG records are detect-and-alert only; native limits have enforcement latency and do not stop separately billed agent flows.
- Ceilings. PAYG 50 / credit 10 per tenant are encoded as
MaxValueon the*_policycountsnapshotcolumns and surfaced in docs. - 2.27 is a proposal. Not an implemented control in this manifest.