Model Orchestration, Stewardship, Allocation, Insights, and Chargeback
MOSAIC is a self-service control plane and administrator/developer experience for Azure API Management's AI gateway capabilities. It stores desired governance state, plans how that state should map to APIM, and presents telemetry from Azure Monitor. It does not proxy model traffic or replace APIM.
This release connects model and published MCP access grants to API Management enforcement. Administrators publish a model or MCP server, grant an existing user, application, Entra Agent ID identity or Entra security group access, review and apply the changes, and later revoke access. Governed models accept a dedicated APIM subscription key for direct grants or an Entra access token, with independently configurable methods and shared per-grant limits. Published MCP servers accept Entra tokens only and use call limits. Authorized direct-grant model callers can retrieve their current subscription key on demand; MOSAIC does not store a duplicate. Security-group grants use Entra tokens only and have no key path.
Publishing and governed access write only through a reviewed,
deterministic plan and an explicit apply, only to a gateway an administrator has switched to
manage. Existing publications remain unchanged until explicitly opted into governed access.
See ADR 0010 and
ADR 0011 for the write and credential-disclosure
boundaries, and ADR 0016 for agent
identities and security-group grants. ADR 0017
documents MCP gateway enforcement.
These show the administrator console (apps/web) and the end-user portal (apps/portal) running
against a fictional Contoso estate that scripts/screenshots seeds
locally, not a real tenant; that is why the console reports Local mode. Endpoint URLs, host
and resource names, tenant and object IDs, MOSAIC record IDs, and keys are blurred. When the UI
changes, regenerate them and update their descriptions as described in
docs/screenshots.md.
Both apps follow the operating system's light or dark setting, and the console can pin either one under Settings > Appearance.
flowchart LR
Admin[Administrator browser] -->|Entra token with Admin role| Web[MOSAIC web]
User[End-user browser] -->|Entra token with User role| Portal[MOSAIC portal]
Web -->|Bearer token| API[MOSAIC API]
Portal -->|Bearer token| API
API -->|Managed identity| Cosmos[(Cosmos DB desired and observed state)]
API -. read-only Graph .-> Graph[Microsoft Graph directory]
API -->|Secret URI only| KV[Key Vault]
API -. read-only ARM .-> Foundry[Registered Azure AI model endpoints]
API -->|read ARM, and write on explicit apply| APIM[Registered API Management gateways]
APIM -->|Runtime model traffic, gateway managed identity| Foundry
APIM --> Monitor[Azure Monitor / App Insights / Log Analytics]
API --> Monitor
Web --> Monitor
Portal --> Monitor
The administrator console and the end-user portal are separate applications with separate
Entra registrations and separate app roles, so they are independently governable. The portal
reaches only /api/v1/portal/* and the current-user /api/v1/me/* routes. Every one of them is
scoped to the caller's own token, and none accepts a subject or requester parameter. The console
first asks GET /api/v1/console/me which MOSAIC role the caller holds, and renders only for
Admin; anyone else sees a single page that says what their account has and what to ask for. See
ADR 0008.
| Concern | Source of truth | MOSAIC responsibility |
|---|---|---|
| Governance intent | Cosmos DB | Store tenant-scoped desired state and audit mutations |
| Runtime traffic and enforcement | APIM | Observe and explain; write only through a reviewed plan and an explicit apply |
| Identity objects and authentication | Microsoft Entra ID | Store object IDs only; validate tokens and app roles; read directory metadata through Graph for lookup and verification |
| Backend credential references | Key Vault | Store secret URIs only, never secret values |
| APIM subscription keys | APIM | Retrieve only for an explicitly authorized reveal; never persist or cache a copy |
| Foundry deployments | Existing Azure AI/Foundry resources | Enumerate deployed models read-only; report, never grant, the gateway's runtime access |
| Traffic/token telemetry | Azure Monitor stack | Emit application telemetry; query/chargeback is deferred |
MOSAIC never silently substitutes in-memory data or local authentication in Azure. Both are
explicit local/test modes and application startup rejects them when MOSAIC_ENVIRONMENT=azure.
- Python 3.12 FastAPI API with OpenAPI, structured JSON logging, Azure Monitor OpenTelemetry,
correlation IDs, anonymous
/healthzand dependency-aware/readyz - Entra issuer, audience, signature, tenant, expiry, and algorithm validation, with app-role
authorization decided per route:
Adminfor every administrative route,Userfor the end-user portal surface - Principal, group, and membership CRUD with validation, stable errors, and audit events, including read-only Microsoft Graph lookup for users, agent identities, agent users and Entra security groups
- Multi-gateway onboarding: register any existing API Management service by resource ID, verify access, and mirror its APIs, endpoints, products, subscriptions, users, groups, backends, and named value metadata into Cosmos
- Entitlements as desired state: grants to a user, MOSAIC group, Entra security group, application or agent over a model API, MCP server, product, or model deployment; token and request limits; catalog visibility; access requests, whose approval creates and links the requester's grant intent; and effective-access resolution that reports whether a grant arrived directly or through a group
- Governed access for direct user/application/agent grants and Entra security-group grants to MOSAIC-published model APIs: reviewed APIM deployment, key or Entra authentication for direct grants, Entra-only group grants, shared token/request limits, revocation, and distinct desired versus applied state
- MCP publishing and governed access for MOSAIC-registered servers: reviewed APIM deployment of a passthrough MCP API, per-publication resource metadata, Entra-only direct and security-group grants, call limits, credential stripping, backend managed identity and fail-closed recovery
- Current-user entitlement and connection APIs, plus audited on-demand key retrieval, which the portal's My access page uses to show connection details and reveal keys for applied direct model grants
- Model endpoint onboarding: register Azure OpenAI and Azure AI Foundry resources, verify MOSAIC's control-plane access, discover the deployments and available models on them, and report — per registered gateway — whether that gateway's managed identity can actually call them, judged by the data actions its roles grant on the resource the published API calls and by whether the gateway has a network path to it
- MCP server registration: register a Model Context Protocol server by URL, connect to it as a read-only client, and record the tools it declares — including the input schemas, output schemas, and behaviour annotations that API Management's management plane does not expose
- MCP servers already present in a registered gateway are detected and counted
- Plain-language policy view: policy XML is parsed in memory and reduced to a digest plus redacted semantic facets, so administrators never see markup and MOSAIC never stores it
- AI surface detection that identifies which APIs and backends front large language models, across Azure OpenAI, Azure AI Foundry, Azure AI inference, OpenAI, Anthropic, Google Vertex AI, and AWS Bedrock
- MCP server discovery, and import of selected model APIs and MCP servers from a synchronised gateway into MOSAIC's own desired state
- Model publishing: expose an observed deployment through a gateway by creating its backend, policy fragment, API, operations, API policy, product, product link, and subscription — through a persisted deterministic plan, an explicit apply, per-step results, and a rollback that deletes only the resources that apply created
- MCP publishing: expose a registered MCP server through a gateway by creating its backend, policy fragment, passthrough MCP API, API policy, protected-resource-metadata API, metadata operation and metadata policy — through the same reviewed plan and explicit apply model
- Async repository abstraction with explicit in-memory and Cosmos implementations
- React/TypeScript/Vite administrator console using Fluent UI, React Router, TanStack Query, and
MSAL, with responsive navigation and persisted light/dark/system themes. It confirms the caller
holds the
Adminrole before it shows any of that; a caller with onlyUser, or with no MOSAIC role, is told the console isn't for them and what to ask an administrator for - Runtime browser configuration; Azure IDs and service URLs are not baked into the web image
- Typed APIM read and write boundaries kept in separate classes, plus Foundry import and deterministic policy authoring that never returns markup
- Separate non-root frontend/backend containers
- End-user portal: a separate SPA on its own Entra registration and the
Userapp role, where a non-administrator sees what they are entitled to, how each grant reached them, the catalog of governed resources, and can request access to something they cannot yet use. My access and My requests head each grant and request with its resource's name, as the catalog shows it, and keep its kind visible, including for a resource since made private. They never show a raw resource ID. For an applied direct model grant, the portal also shows the endpoint, operations, accepted credentials, limits, and placeholder code samples, and reveals a key on request - Environments: an administrator-defined catalog (Development, Test, QC, Staging, Production,
Sandbox, and custom environments) that classifies gateways, model endpoints, and MCP servers.
Compatibility rules are enforced when models and MCP servers are published, and Azure
environmenttags and legacy labels become suggestions an administrator confirms. Re-classification is guarded, and advisory findings flag blocked pairings MOSAIC's rules didn't stop. The portal shows each catalog entry's and grant's environment, so people request development and production access separately - End-user usage report: a caller-scoped
/me/usagecontract and the portal's Usage & cost page. Until Log Analytics is wired in, it reports simulated usage built from the caller's real grants and limits. Each governed model and MCP call is already tagged at the gateway with the grant it matched, so token, security-group, and MCP grants can be attributed once real data arrives - ACR remote builds for every image, so deployment does not depend on a local Docker daemon
azdand modular Bicep for three Linux Web Apps on one plan, ACR, Cosmos, Key Vault, APIM, Log Analytics, Application Insights, diagnostics, managed identities, and narrow RBAC- Idempotent Entra application/service-principal setup through
azdhooks
The Gateways workspace, the Identity workspace, the Models and MCPs workspaces, the Entitlements workspace, Settings → Environments, model publishing, the end-user portal, and the deterministic policy preview use live API contracts. The portal's Usage & cost page also uses a live API contract, but the figures it reports are simulated from the caller's real grants and labeled Sample data. Analytics, policy metadata, and other future operational experiences are interactive frontend previews labeled Sample data or Local preview. They never claim to mutate Azure, query Azure Monitor, or substitute sample data for a failed API request.
Existing deployments need azd provision (or a manual role grant) before publishing works: the
API's identity moves from the API Management reader role to contributor. Until it is granted,
preflight reports the missing write permissions precisely rather than failing during an apply.
- Azure subscription where the deployer can create resources and role assignments
- Microsoft Entra role capable of managing app registrations and service principals (Application
Administrator or broader), assigning the initial app role, and granting the model client's
tenant-wide consent. Agent identity and security-group lookup also need a privileged
administrator to consent the API managed identity's Microsoft Graph application permissions:
User.ReadBasic.All,GroupMember.Read.AllandAgentIdentity.Read.All. - Azure CLI
- Azure Developer CLI
- Python 3.12 and uv
- Node.js 24 and npm
- Docker for local container validation
The reference development target is region eastus2 with environment name mosaic-dev. Supply
your own subscription when deploying.
Install dependencies:
uv sync --all-packages --group dev
Set-Location apps\web
npm ciRun the API with the opt-in local modes:
$env:MOSAIC_ENVIRONMENT = "local"
$env:MOSAIC_AUTH_MODE = "local"
$env:MOSAIC_REPOSITORY_BACKEND = "memory"
$env:MOSAIC_TENANT_ID = "local-development"
uv run mosaic-apiLocal authentication grants both the Admin and User app roles by default. Set
MOSAIC_LOCAL_ROLES to narrow it — '["User"]' simulates an end user who must not reach an
administrative route. The setting is rejected outside local and test environments.
Directory lookup and group-claim enforcement have explicit switches:
| Setting | Default | Purpose |
|---|---|---|
MOSAIC_ENTRA_DIRECTORY_LOOKUP |
true |
Enables read-only Microsoft Graph lookup and verification for users, agent identities, agent users and Entra security groups. Set to false to require administrators to type object IDs. |
MOSAIC_ENTRA_GROUP_CLAIMS |
true |
Records whether bootstrap configures groupMembershipClaims: SecurityGroup on the runtime/API registrations. Set to false only when group grants should be stored but not matched at the gateway. |
In a second terminal:
Set-Location apps\web
npm run devapps/web/public/config.js selects local auth and http://localhost:8000. Do not deploy that file
as configuration: the web container regenerates it from App Service environment variables on
startup.
uv run ruff check apps/api
uv run mypy apps/api/src
uv run pytest apps/api/tests
Set-Location apps\web
npm run typecheck
npm run lint
npm run test
npm run build
Set-Location ..\..\e2e
npm run test:unit
npm run typecheck
npm run lint
Set-Location ..
az bicep build --file infra\main.bicep
python -m unittest scripts.tests.test_mosaic_entra scripts.tests.test_verify_model_access
python -m unittest scripts.tests.test_spa_nginxSign in and create the normal development environment:
az login
azd auth login
az account set --subscription <your-subscription-id>
azd env new mosaic-dev
azd env set AZURE_SUBSCRIPTION_ID <your-subscription-id>
azd env set AZURE_LOCATION eastus2
azd env set MOSAIC_APIM_PUBLISHER_NAME "MOSAIC administrator"
azd env set MOSAIC_APIM_PUBLISHER_EMAIL "your-admin-address@example.com"
azd env set MOSAIC_PYTHON_INDEX_URL "https://pypi.org/simple"
azd upThe preprovision hook idempotently creates separate single-tenant Entra registrations:
mosaic-dev-api:access_as_userdelegated scope, plus anAdminand aUserapp rolemosaic-dev-spa: administrator console SPA redirects and delegated permission to the APImosaic-dev-portal: end-user portal SPA redirects and delegated permission to the APImosaic-dev-model-runtime: the separate audience for APIM model calls, with theModels.Invokedelegated scope,Models.Invoke.Applicationapplication permission,Mcp.Invokedelegated scope andMcp.Invoke.Applicationapplication permission. Bootstrap configuresgroupMembershipClaims: SecurityGroupso user, application and agent runtime tokens can carry Entra security-group object IDs.mosaic-dev-model-client: a public client people sign in with to get model-runtime tokens. It has no secrets, certificates or app roles, and its only permission is delegatedModels.InvokeandMcp.Invoke, with tenant-wide admin consent. Interactive (http://localhost) and device code sign-in both work.
It assigns the deploying user the initial Admin role. The postprovision hook adds the deployed
web redirect and the deployed portal redirect, the latter from the PORTAL_APP_URL output of
the portal App Service. A directory authorization failure stops deployment and identifies the
failed operation; identity setup is never skipped. Graph directory lookup needs admin consent for
User.ReadBasic.All, GroupMember.Read.All and AgentIdentity.Read.All on the API managed
identity. The model client's delegated consent also needs Cloud Application Administrator,
Application Administrator or Privileged Role Administrator. Without one of those roles the hook
prints warnings with exact commands for an administrator, and deployment continues. People can't
get tokens with the model client until its consent exists, and directory lookup stays degraded
until the Graph consent exists.
To skip the model client, run azd env set MOSAIC_ENTRA_MODEL_CLIENT false before provisioning.
The hook then leaves any existing model client registration and consent alone, and keeps
MOSAIC_MODEL_CLIENT_ID as set. So you can point it at a client you manage yourself, as long as
that client is consented for Models.Invoke and Mcp.Invoke when it is used for both models and
MCP. Also set MOSAIC_ENTRA_MODEL_CLIENT to false before you revoke the model client's consent,
or the next azd provision grants it again.
Assign the User app role — normally to an Entra group — to everyone who should reach the portal.
Tenant membership alone does not grant it.
People with a user grant sign in with the model client to request
api://<model-runtime-client-id>/Models.Invoke. Bootstrap writes its client ID as
MOSAIC_MODEL_CLIENT_ID, and connection details show it as entraClientId. See
Call a published model with an Entra token. Any other
delegated client needs its own consent for that scope. Applications use the
Models.Invoke.Application permission and request api://<model-runtime-client-id>/.default.
Agent identities use the same .default runtime scope through the Agent ID token flow, and agent
users use the delegated scope through their parent agent identity. See
Call a published model with Microsoft Entra Agent ID.
For published MCP servers, people and agent users request
api://<model-runtime-client-id>/Mcp.Invoke; applications and agent identities request
api://<model-runtime-client-id>/.default and need Mcp.Invoke.Application. See
Connect to MCP servers published through MOSAIC.
Assign these permissions through normal Entra administration. A MOSAIC grant does not silently
consent a client, create an identity, assign app roles, or grant Microsoft Graph permissions. The
only delegated runtime consent bootstrap creates is for the model client's Models.Invoke and
Mcp.Invoke grant. Bootstrap exposes
MOSAIC_MODEL_RUNTIME_CLIENT_ID; this must not be the MOSAIC control-plane API's client ID.
The console and portal containers serve index.html and every SPA route with
Cache-Control: no-cache, /config.js with no-store, and the content-hashed files under
/assets/ with public, max-age=31536000, immutable. After a redeploy, the next page load
revalidates index.html and picks up the new bundle and runtime configuration. The policy is in
apps/web/nginx.conf and apps/portal/nginx.conf, which are kept identical.
APIM Developer is the dominant cost (currently roughly USD 51/month at continuous use) and can take 30–60 minutes or longer to provision. The shared B1 Linux plan is roughly USD 13–15/month; Basic ACR is roughly USD 5/month. Cosmos serverless, Key Vault, and monitoring are consumption-based. Expect an idle baseline near USD 70/month before meaningful telemetry volume.
Remove the environment when not in use:
azd down --purgeEvery entity contains tenantId; this initial deployment is single-tenant but the data contract is
not. The domain distinguishes:
ModelEndpoint: a registered Azure OpenAI, Azure AI Foundry, or OpenAI-compatible endpoint, its verified control-plane access, and per-gateway runtime readinessModelEndpointSyncRun: the outcome of one model discovery runCatalogModel: provider model identity/versionModelDeployment: callable deployed endpointPrincipal(users, applications, managed identities, agent identities, agent users and Entra security groups),Group,GroupMembershipEnvironmentCatalog: the tenant's environments, which are production-class, which other environments each one's gateways also accept endpoints from, and whether classification is requiredGateway: a registered API Management service, its verified access, and its inventory summaryGatewaySyncRun: the outcome of one inventory synchronisationModelApi: an API Management API an administrator adopted as a governed model endpointMcpServer: an API Management MCP server an administrator adoptedMcpEndpoint: a registered MCP server MOSAIC connects to and reads tools fromPublication: intent to expose one model deployment through one gateway, plus the API Management resources an apply created and whether MOSAIC created each onePublishPlan,PublishRun: the reviewed changes and the audited result of applying themEntitlement: a grant of a governed resource to a user, group, or application, its token and request limits, and the binding that realizes it in API Management: a product or subscription, or the grant tag an applied publication emits at the gatewayAccessRequest: a portal user's request for a resource they can see but are not entitled toCredentialReference: Key Vault secret URI onlyPolicyRevision,SyncOperation,AuditEventCosmos uses:
| Container | Partition key | Purpose |
|---|---|---|
desired-state |
/tenantId |
Control-plane entities, tenant-local queries, and transactional audit outbox |
sync-operations |
/tenantId |
Gateway and endpoint sync runs, publish plans, and publish runs |
observed-state |
/tenantId |
What MOSAIC observed in each registered gateway |
audit-events |
/tenantId |
Append-only administrator mutation history |
observed-state is deliberately separate from desired-state. It is disposable, rebuilt on every
sync, and churns far more than administrator-authored governance intent. Observed documents use
deterministic IDs so a re-sync upserts in place; anything absent from the new snapshot is swept.
Directory mutations and their audit records commit atomically to desired-state. The repository then
projects each outbox record into audit-events; a projection failure leaves the durable outbox record
for readiness-driven retry and is emitted to structured logs rather than failing an already-committed
administrator request.
The initial strategy prioritizes tenant-local operations. Hierarchical or sharded keys should follow measured scale, not speculation.
- Only health endpoints are anonymous.
- Browser authentication uses authorization code + PKCE through MSAL.
- The API accepts RS256 tokens from the configured tenant only, validates OIDC discovery/JWKS, issuer, client-ID audience, signature, time claims, and tenant. A token carrying none of MOSAIC's app roles is rejected before any route is reached.
- Authorization is a per-route decision, not part of authentication.
require_admindemands theAdminrole and guards every administrative route;require_portal_userdemands theUserrole, whichAdminalso satisfies. Neither role is implied by tenant membership, so an operator assignsUser— usually to an Entra group — before anyone can use the portal. require_mosaic_roleadmits either role and guards onlyGET /api/v1/console/me, which reports the caller's MOSAIC roles from their validated token. The console uses it to decide what to show: the console forAdmin; forUseralone, a page saying the console is for MOSAIC administrators and thatUseropens the end-user portal; with no MOSAIC role, a page saying an administrator must grant one. That page is presentation, not a boundary — the administrative routes still refuse those callers.- Production uses system-assigned managed identities. Local Azure SDK access uses
DefaultAzureCredential; Azure usesManagedIdentityCredential. - MOSAIC reads Microsoft Graph only through its managed identity and only for directory lookup,
principal verification, security-group members, group-based access explanation and overlapping
grant detection. It needs
User.ReadBasic.All,GroupMember.Read.AllandAgentIdentity.Read.All, and never writes to Entra. API Management never calls Graph. - Cosmos local/key authentication and ACR admin credentials are disabled.
- Key Vault uses RBAC, soft delete, and purge protection.
- Backend access is scoped to Cosmos data contributor, Key Vault Secrets User, API Management contributor, Log Analytics Reader, and Monitoring Reader.
- API Management writes are bounded by two independent conditions rather than one: the role
assignment, and a gateway an administrator explicitly moved to
manage. MOSAIC refuses that switch until preflight has confirmed write access, and every write runs against a reviewed plan whose digest still matches the intent it was produced from. - The contributor role carries
subscriptions/listSecrets. Only an explicit credential-reveal operation uses it, after checking caller ownership and a trusted, applied grant. Inventory and publishing do not read keys. Reveals are audited without their secret values; responses are non-cacheable, and neither Cosmos nor Key Vault stores a copy. See ADR 0011. - Model-runtime Entra tokens have a different audience from MOSAIC control-plane tokens. APIM
validates the runtime token and authorizes its tenant/object ID against an applied direct grant,
or its
groupsclaim against an applied security-group grant; being signed into MOSAIC or holding itsAdminrole does not itself grant model access. - Entra security-group grants are authorized only from the runtime token's
groupsclaim. Removing a member takes effect when that member gets a new token, and callers whose tokens contain group overage markers need direct grants because the gateway cannot resolve group membership through Graph. - Agent identities are service principals. Direct agent grants and app-only group-member calls need
the
Models.Invoke.Applicationapp role, assigned directly to the agent identity or inherited through a configured Agent ID blueprint. App roles assigned to a group do not flow to service principals in access tokens. - The MOSAIC model client is a public client with no secrets, certificates or app roles. Its
tenant-wide consent covers only delegated
Models.Invokeon the runtime registration. That lets Entra issue a person's token but authorizes no model call by itself: APIM still requires an applied grant to the person or to a security group in their token. Administrators can revoke the consent under the client's enterprise application permissions (after settingMOSAIC_ENTRA_MODEL_CLIENT=false, so provisioning doesn't grant it again), and target the client with Conditional Access. - On model endpoints MOSAIC asks only for
Reader. It deliberately holds no data-plane inference right and nolistKeyspermission on any Azure AI resource, so it cannot call a model or read an account key even where it can enumerate deployments. - MOSAIC never reads named value secret values, and never persists or renders policy XML. Policy documents — including the ones MOSAIC authors when publishing — are reduced to a digest plus redacted facets in memory.
- Credentials for non-Azure endpoints are stored as Key Vault secret URIs only. MOSAIC resolves a secret at call time and never persists, returns, or logs its value.
- Frontend and backend pull from ACR through their managed identities.
A gateway is an existing Azure API Management service that an administrator registers with MOSAIC by
resource ID. MOSAIC supports several across subscriptions and environments; the APIM that azd
deploys is registered automatically on first startup and also appears as a one-click suggestion.
Each gateway belongs to one environment, or is Unclassified; see Environments.
Onboarding runs a preflight against Azure Resource Manager with MOSAIC's managed identity. It reads
effective permissions at the resource scope, and when they are missing it reports the exact role,
scope, and az role assignment create command an operator needs. MOSAIC cannot grant itself that
role, so it explains rather than fails silently.
The needed roles are:
| Purpose | Role | Role definition ID |
|---|---|---|
| Observing a gateway | API Management Service Reader Role | 71522526-b88f-4d52-b57f-d31fc3546d0d |
| Publishing models into a gateway | API Management Service Contributor | 312a565d-c81f-4fd8-895a-4e21e48d571c |
A gateway MOSAIC can only read is fully usable for observation. Writing requires two independent
conditions: the contributor role above, and an administrator switching the gateway from observe to
manage. MOSAIC refuses the switch until preflight has actually confirmed write access, and refuses
every write to a gateway left in observe mode however the role is assigned.
Administrators switch modes with Management mode on the gateway's Overview tab, and confirm each
direction. The switch itself changes nothing in API Management; it only records the mode in MOSAIC.
Manage stays disabled until the access check confirms write access. Until then, the page shows the
role, scope and identity to grant, and Check access runs the preflight again. In manage,
MOSAIC writes to the gateway only when an administrator applies a reviewed publish plan, unpublishes
a model, or confirms recovery of an interrupted apply. Switching back to observe leaves published
models in API Management, where they keep serving calls. MOSAIC then refuses to plan, apply or
unpublish them, so grant and access changes saved in MOSAIC wait until the gateway is managed again.
Either switch is refused while an apply or unpublish on one of the gateway's publications is still
running, or is awaiting recovery after an interruption.
The contributor role also grants subscriptions/listSecrets. MOSAIC uses that capability only
for an authorized, explicitly requested reveal of an applied, owned grant's key. A custom role
that permits writes but excludes that action cannot reveal keys; the API reports the missing
key-read action separately. This is a product authorization boundary, not a claim that MOSAIC's
managed identity is technically unable to read secrets.
Synchronisation collects APIs and their operations, MCP servers and their tools, products,
subscriptions, gateway users and groups, backends, named value metadata, and policies at the
service, product, and API scopes. Operation-scope policies are read on demand rather than during a
full sync. A failure reading one collection degrades the run to partial and is recorded, rather
than discarding the whole snapshot — and the entity types it could not read are exempt from the
stale-document sweep, so a transient failure never looks like a deletion.
API Management models an MCP server as an API of type mcp, visible only on management API version
2025-09-01-preview or later. MOSAIC keeps its inventory on the stable 2024-05-01 contract and
uses the preview version for MCP discovery alone, so a preview API that changes cannot break the
sync administrators depend on.
A service that rejects the preview version is not a failure. MOSAIC records
capabilities.mcpServers as unavailable, the run still succeeds, and the MCPs workspace explains
why the gateway has nothing to offer. Any other read failure is reported as an error and exempts
MCP servers from the sweep, keeping "MOSAIC could not read this" distinct from "there are none".
Observed state is disposable and rebuilt on every sync. Importing promotes a selection of it into
desired-state as ModelApi and McpServer records: MOSAIC now governs these, and the records
survive the sweep.
Importing is a Cosmos write and nothing else. No API Management resource is created, changed, or deleted, no policy is written, and no Azure write API is called. ADR 0001 is unaffected and the contributor role stays ungranted.
Detection decides which rows arrive pre-checked, not which imports are allowed. Every observed API
is offered, an administrator can adopt one MOSAIC did not recognise or skip one it did, and the
record keeps whether the choice was detected or manual. A name absent from the gateway's most
recent snapshot is rejected outright rather than skipped, because importing four of five selected
APIs and reporting success would leave an administrator believing they had governed something they
had not.
Record IDs are deterministic, so re-importing after a sync refreshes a record in place instead of duplicating it. Deleting a gateway deletes what was imported from it.
MCP servers are detected too. API Management models an MCP server as an API with type: mcp, which
is only visible from management API version 2025-09-01-preview, so MOSAIC issues that one call
separately from the stable version it pins everywhere else. A service tier or release channel
without that version is not a fault: MCP visibility is reduced and the sync still succeeds.
Importing covers servers a gateway already hosts. Registering covers the rest: an administrator gives MOSAIC a Model Context Protocol server URL, and MOSAIC connects to it directly. That matters for two reasons — a server can be governed before any gateway fronts it, which is what publishing into API Management will need, and the management plane cannot describe a tool properly. ARM returns only a name, display name, description, and backing operation. Input schemas, output schemas, and behaviour annotations exist nowhere in it, and only the server can supply them.
MOSAIC acts as a minimal read-only MCP client. It performs initialize, sends
notifications/initialized, pages tools/list, and ends the session. It never calls a tool.
An MCP server has no control plane, so unlike a model endpoint there is no way to ask "may MOSAIC read this" without connecting. Registration therefore runs the handshake and stops; discovering tools is a separate, explicitly requested sync.
MOSAIC implements the protocol's handshake era. The current revision, 2026-07-28, is stateless
— it removed the handshake, the session header, and the GET stream — while API Management speaks
the handshake era. MOSAIC offers 2025-11-25 and accepts a counter-offer down to 2024-11-05. A
server outside that range is recorded as unsupportedProtocol, and an SSE-only server as
unsupportedTransport. Both are capabilities, not failures: neither is something an operator fixes
by retrying. A server that does not advertise a tools capability records zero tools without
tools/list ever being called, which stays distinct from a read that failed.
Because this is MOSAIC's first outbound call to a host it did not derive from an Azure resource ID,
the boundary is deliberate. HTTPS is required outside local development; loopback, link-local, and
private addresses are refused, including the instance metadata address; redirects are never
followed; a managed-identity token is attached only to an audience the operator explicitly named;
and responses and page counts are bounded. Only a Key Vault secret URI is stored, resolved at
call time. 401 is reported as "needs authorization", with the scope and resource metadata URL the
server asked for, never as "unreachable".
Tool annotations are recorded as the server's claims, never as MOSAIC's findings. The
specification defaults destructiveHint and openWorldHint to true and requires clients to
treat annotations as untrusted, so an absent hint is stored and displayed as "not stated" rather
than as its default — a tool that said nothing is never rendered as if it promised to be safe.
MOSAIC parses API Management policy XML in memory and keeps only a SHA-256 digest and redacted semantic facets. Administrators see sentences:
Limits model usage to 10,000 tokens per minute, counted per subscription.
The gateway authenticates to
https://cognitiveservices.azure.comwith its own managed identity.
Rules MOSAIC does not interpret are labelled externally authored and counted by name. They are never hidden, and never shown as markup. Policy documents routinely carry inline credentials, so not storing the source is a security property as well as a product decision. URLs lose their query string before they are stored or displayed, because backend URLs commonly carry Azure Functions keys and storage SAS tokens there.
When MOSAIC begins writing, it will author named mosaic-* policy fragments that customer policies
include, rather than rewriting whole policy documents. Fragments are inventoried now so that
ownership boundary already exists.
A model endpoint is an Azure OpenAI or Azure AI Foundry resource that a gateway fronts. Administrators register one by resource ID, or accept a suggestion. MOSAIC then reads the deployments on it. It never calls a model, and it never changes the resource.
Every endpoint has two access relationships, held by two different identities:
| Relationship | Identity | Plane | What it enables |
|---|---|---|---|
| Onboarding | MOSAIC's managed identity | Control plane | Listing the deployed models |
| Runtime | The gateway's managed identity | Data plane | Actually calling those models |
They are reported separately, because an endpoint MOSAIC reads perfectly well can still be uncallable through a gateway.
MOSAIC asks only for Reader. For the gateway it recommends a built-in role, but it accepts any
role whose data actions cover the operations the published API calls:
| Purpose | Recommended role | Role definition ID |
|---|---|---|
| MOSAIC enumerating models | Reader | acdd72a7-3385-48ef-bd42-f606fba81ae7 |
Gateway calling an Azure OpenAI resource (kind: OpenAI) |
Cognitive Services OpenAI User | 5e0bd9bd-7b93-4f28-af87-19fc36ad61bd |
| Gateway calling an AI Services or Foundry resource, including one registered by Foundry project, and its Claude models | Foundry User | 53ca6127-db72-4b80-b1b0-d745d6d5456d |
| Gateway calling a resource MOSAIC cannot read yet | Foundry User, which is accepted for either kind | 53ca6127-db72-4b80-b1b0-d745d6d5456d |
How the gateway's access is judged:
- The scope is the account the published API calls. This holds even for an endpoint registered by Foundry project, because models are deployed on the parent resource. A grant on the project does not reach the account, so it is reported as narrower than what is needed and is not counted. Grants on the resource group or subscription do count, and are labelled as inherited.
- MOSAIC reads each assignment's role definition. It checks
dataActionsminusnotDataActionsagainst the exact data actions of the operations it publishes. So Cognitive Services User, Cognitive Services OpenAI Contributor, or a custom role counts wherever it covers them. - An AI Services account must cover every API MOSAIC publishes from it. That includes the
Anthropic Messages API for Claude, even before a Claude model is deployed. Both of its routes need
Microsoft.CognitiveServices/accounts/AIServices/providers/action.- Foundry User and Cognitive Services User grant it.
- Azure AI Developer doesn't. MOSAIC reports it as missing and names the API that needs the action.
- The
services.ai.azure.comhost and thehttps://ai.azure.comtoken audience that API uses don't change the scope or the roles: it's the same account.
- Unreadable definitions fall back to a list. When MOSAIC cannot read a role definition, only built-ins already known to be sufficient are trusted.
- Conditions and deny assignments stop short of "can invoke". A role assigned under an ABAC condition MOSAIC cannot prove holds for those calls is reported as not confirmed, never as access. A deny assignment covering those calls overrides the role.
- The network path is part of the verdict. If public network access is disabled and the gateway is not connected to a virtual network, the gateway cannot invoke, whatever roles it holds. Private endpoints and firewalls MOSAIC cannot fully evaluate are reported as not confirmed, not as a denial.
- Advice matches the check. Whatever MOSAIC recommends, the check accepts. When the kind is not
known yet, MOSAIC says so, and granting it
Readerfirst lets it recommend the exact role.
ADR 0013 records these rules.
On the Models page, select an endpoint to open its Access card.
- Gateways calling this endpoint gives each gateway a verdict: can invoke, cannot invoke,
or not confirmed.
- When a role satisfies the check, it names the role and the scope where it is assigned.
- When no role does, it gives the reason for each role the gateway holds: missing data actions, a narrower scope, or a condition.
- When a role is missing, it shows the recommended role and the
az role assignment createcommand. MOSAIC never runs the command itself.
- Endpoint settings, above the gateway verdicts, shows the resource kind, public network access,
firewall, and key authentication. Those settings decide whether a gateway can reach the endpoint
at all. Azure omits
disableLocalAuthfrom accounts where it was never set, and its default isfalse, so MOSAIC shows an unset value as key authentication Enabled.
Every role whose name begins Cognitive Services or Foundry that grants control-plane deployment
read also grants data-plane inference, and most also grant listKeys. Reader is the only built-in
that grants the read alone. It also covers the role-assignment, role-definition, and
deny-assignment reads that the runtime check needs. MOSAIC also emits a narrower custom role
definition for operators who want one. That role omits the role-assignment read, so runtime access
then reports as not confirmed rather than guessing. Roles are compared by GUID rather than name,
because Microsoft renamed the Foundry roles in 2026 (Azure AI User became Foundry User) without
changing their IDs.
MOSAIC finds endpoints three ways: a pasted resource ID, hosts it already observed as AI backends
inside a registered gateway, and an enumeration of Azure AI accounts across visible subscriptions.
The last needs Reader at subscription scope, which MOSAIC does not grant itself — a subscription
it cannot read is reported with the command that would fix it and skipped, so one missing assignment
never blanks the list. When MOSAIC can't see any subscription, or couldn't list them, the Models page
now says so and gives the Reader command for the subscription MOSAIC was deployed into and for
each registered gateway's subscription, instead of showing an empty list.
A subscription MOSAIC can list is not necessarily one it can read in full: Azure answers a
subscription-wide list with only the resources the caller can read, and doesn't say anything was
left out. So MOSAIC also checks the permissions it holds at each subscription itself. When those
don't let it read every Azure AI resource there, typically because its roles are on a resource
group or on individual resources, the Models page says MOSAIC can read only part of that
subscription, still offers what it found, and gives the subscription-scope Reader command instead
of reporting nothing new to register. If MOSAIC can't read its own permissions, it makes no claim
either way.
Azure can take several minutes, and occasionally longer, to apply a new role. So when MOSAIC asks
for Reader on its own identity to read an endpoint or to scan subscriptions, it says so: if
Check access or the scan still fails right after the grant, wait a few minutes and try again.
OpenAI-compatible endpoints are registered with a Key Vault secret identifier the operator created. MOSAIC stores the URI only; discovery for those endpoints is not implemented yet.
One registration per Azure AI resource. Deployments live on the resource (the account), never on a Foundry project, so a project and its parent resource list the same models. MOSAIC treats every registration as covering its resource:
- Registering a resource whose project is already registered, a project whose resource is, or a
second project on the same resource is refused with a
409. The message names the endpoint that already lists those models, anddetailscarries itsidandname. - Discovery doesn't suggest a resource that a registered project already covers.
- Registering the exact same resource ID again is refused as before.
Overlapping records registered before this check are left as they are. Remove one of them to stop the duplicate listing.
Removing an endpoint. Remove on the Models page asks first. The dialog says what goes: MOSAIC's
record of the endpoint, its synced models and its sync history. Nothing changes in Azure. The API
(DELETE /api/v1/model-endpoints/{id}) refuses with a 409 while any publication from the endpoint
may still own resources in API Management, because without the endpoint that publication could
never be planned, applied or unpublished again, and its API would keep serving traffic. A
publication blocks when it:
- recorded resources it created, including a failed apply that left some behind;
- is applying, or its access change is applying or was interrupted (
accessStateunknown); - still has an enabled grant applied at the gateway;
- is locked by a run in progress.
The refusal lists the blocking publications (id, display name, status) in details, and the dialog
shows them. Unpublish those models first. Publication records that own nothing — drafts, planned or
rolled-back publications, and unpublished ones — are deleted with the endpoint and audited as
publication.removed, because they could never be planned again without it.
Publishing takes a deployment MOSAIC observed on a registered model endpoint and exposes it through a registered gateway. It is the first thing MOSAIC writes to API Management, and it completes the loop ADR 0001 described and deliberately stopped one step short of: desired state, observed state, deterministic plan, explicit apply, audited result.
A Publication in desired-state records the intent. Saving it changes nothing in Azure. Planning
it produces a persisted, deterministic PublishPlan; applying runs against that specific plan and
rejects one whose digest no longer matches, so an administrator cannot approve one set of changes
and have another applied. A PublishRun records the outcome of every step.
The admin console applies a plan only from its review, which lists the plan's steps and policy facets, and for governed access every target grant. In the Published models table, Re-plan makes a fresh plan and opens that review; it is also how a failed or rolled-back publication is retried. If MOSAIC refuses the reviewed plan, for example because the publication changed after it was planned, the review says why and shows a fresh plan in its place.
Applying creates, in dependency order:
| Order | Resource | Purpose |
|---|---|---|
| 1 | Backend | The model endpoint origin, with query and fragment stripped |
| 2 | mosaic-* policy fragment |
Managed-identity authentication, routing to the backend, and, where the gateway's tier supports them, token limit and token metric |
| 3 | API | The route, created with no serviceUrl so removing the fragment fails closed |
| 4 | Operations | A curated, versioned set per API shape |
| 5 | API policy | A thin <include-fragment> of the MOSAIC fragment |
| 6 | Product | Carries the API |
| 7 | Product/API link | |
| 8 | Subscription | Only when the publication requires one |
Each resource is created after the resources it names. API Management accepts a policy fragment and
only then checks the backend its set-backend-service names, failing the write if that backend does
not exist yet, so the backend comes first. Likewise, the API policy includes the fragment; the
operations and the API policy belong to the API; the product link joins the product and the API;
and the subscription is scoped to the product. A plan saved by an earlier MOSAIC release that put
the fragment first is refused at apply; plan the publication again.
Operation sets are shipped and versioned by MOSAIC rather than fetched from the provider, so a plan is deterministic and does not couple an APIM write to a third-party document being reachable. Each publication records the API shape it was created with, chosen from the deployment's model format and capability (ADR 0012):
| Shape | Deployments | Operations |
|---|---|---|
| Azure OpenAI | Azure OpenAI chat, responses, completion, embeddings, image and transcription models | Chat completions, completions, embeddings, image generations, audio transcriptions and translations, and responses |
| Foundry Models | Foundry (AI Services) chat and embeddings models | The Foundry Models inference routes under /models |
| Anthropic Messages | Claude models on Foundry | /anthropic/v1/messages and /anthropic/v1/messages/count_tokens, served from the resource's services.ai.azure.com host |
Deployments no shape can serve, such as realtime, video and text-to-speech models, are listed as not publishable with a reason, and creating a publication for one is refused. A publication also records the shape version that produced it. OpenAI-compatible endpoints have no curated shape, and are refused rather than guessed at.
API Management meters the Anthropic Messages API with llm-token-limit and llm-emit-token-metric
only on v2 tiers. On a classic tier such as Developer, the publish wizard explains this, and a Claude
publication applies no token limits or token metrics. Governed grants on it can use call limits
instead.
Every step records whether it created the resource or found one already there. A step succeeds only
once Azure has finished its write: API Management finishes a policy fragment or an API write
asynchronously, on an update as well as a create, so MOSAIC waits for any write Azure answers with
Azure-AsyncOperation or Location, whatever its status code. A failed step says why: when Azure
explains a failed operation, or refuses a request outright, the step's error carries Azure's error
code, message and most specific detail, such as a validation error naming the policy element, line
and column. It is bounded in length and never includes policy markup. An update API Management
rejects after accepting it leaves the previous content in place and fails its step, so it is never
reported as applied: a re-applied publication rolls back, and a failed governed apply denies access,
then restores only the last safe access. If a step fails, MOSAIC reverses the completed steps and
deletes only resources that run created — ownership is recorded at the moment of the write,
never inferred from a name, so a product that merely matches a MOSAIC name is never destroyed. A
resource MOSAIC replaced rather than created is not reverted, because the previous content was never
stored; those are named in the run instead. If the rollback itself fails, the run reports
rollbackFailed and lists exactly what was left behind.
Unpublishing runs the same machinery over the tracked resources in reverse, so a fragment is removed before the backend it routes to. A publication that still owns API Management resources cannot be deleted, and a gateway with published models cannot be removed, so intent is never dropped while the resources it created keep running.
Use Entitlements to link a previously published model if necessary, opt it into governed access, and choose its key/Entra methods. Create a direct user, application, agent, or Entra security-group grant, then review and apply the model-wide plan. The review includes every grant/settings change it will deploy. Saving a grant is not an APIM write, and a pending revocation is not yet a runtime revocation. Use the person's Entra object ID, the application's service-principal object ID, the agent identity's object ID, or the security group's object ID; application client IDs are not interchangeable except for agent identities, whose app ID and object ID are the same. Prefer the subscription-key header over putting credentials in URLs.
Opting in deliberately stops the publication's former generic/bootstrap key from authorizing requests. Each direct grant gets its own API-scoped subscription. Both primary and secondary keys and the subject's Entra token share that grant's counters. A key is still a transferable bearer credential, not proof that the named person is using it. Disabling both authentication methods denies everyone; it never makes the API anonymous.
Security-group grants are Entra-only. They create no APIM subscription and reveal no keys. APIM
matches them from the validated runtime token's groups claim, applies limits per member by the
member's oid, and chooses the direct grant first or otherwise the most generous matching group
grant. A disabled grant counts as absent. Group overage in the token means no group grant can match;
the remedy is a direct grant.
The reviewed policy explicitly translates the earlier subscription ID/key counter defaults into shared grant counters, so previously saved grants can be opted in without silently rewriting their desired state. Other custom counter expressions are rejected instead of weakening enforcement.
Token-governed model access is constrained by APIM's supported chat-completions/response schemas.
Unsupported image, audio, or embedding operations must not be mistaken for metered calls.
For responses, AI Services chat and Anthropic Messages routes that do not include a deployment in
their path, the request body's model must exactly match the deployment name shown in connection
information. On an Anthropic publication, governed access permits only the Messages operation.
Publication limits remain safeguards even when a grant has no additional limits. Native rate and
quota enforcement is distributed and gateway-scoped, not an exact global accounting ledger.
Administrators can explicitly reveal/copy an applied grant's key. Portal clients with the MOSAIC
User role can use:
| Method | Route under /api/v1 |
Result |
|---|---|---|
| GET | /me/entitlements |
The caller's own direct grants and deployment state; no keys |
| GET | /me/entitlements/{id}/connection |
Endpoint, operations, runtime audience/scope, model client ID, and limits |
| POST | /me/entitlements/{id}/keys/reveal |
The requested key; body {"slot":"primary"} or {"slot":"secondary"} |
| GET | /me/usage?period=30d |
The caller's usage and estimated cost per grant, simulated for now; see Usage and cost in the portal |
People use the connection's tenantId, entraClientId and entraScope to get a runtime token.
See Call a published model with an Entra token.
entraClientId is set only for user grants whose applied audience is the current runtime
registration, because that is the only one the model client is consented for. Applications
and agent identities sign in as themselves with .default; agent users use delegated tokens from
their parent agent identity. See
Call a published model with Microsoft Entra Agent ID.
The administrator equivalents omit /me and require Admin. Knowing another entitlement or
application ID does not authorize a reveal. Application-owner delegation remains future work.
A current-user route always uses the token's identity, never a caller-supplied user ID.
When an apply fails, the /me and /portal routes report it through runtime.status alone and
return runtime.error as null, even to an administrator. That field holds API Management's raw
error, which names MOSAIC's internal resources and is shared by every grant on the model, so only
the administrator routes return it.
In the portal, each model grant on My access has a Connection details button. The
connection loads only when it is expanded, and it shows the endpoint, full operation URLs,
deployment, key header, accepted methods, Entra tenant, client ID, scope and audience, limits, and
whether the grant is applied to APIM. If the last apply failed, the panel says so and asks the
person to have an administrator retry it. When the connection has an entraClientId, a Get a token
(Python) sample signs the person in with the model client using a device code. Without one, the
panel asks them to get the client ID from an administrator. Show primary key and Show
secondary key reveal one key for 60 seconds. The key is also hidden by Hide key, when the
panel closes, and when the user navigates or leaves the page. The key is held only in component
state, never in the query cache, browser storage, the URL, or logs. Code samples use
$MOSAIC_API_KEY or $MOSAIC_ACCESS_TOKEN placeholders and never include a revealed key. For a
Claude model, the samples call /anthropic/v1/messages without an api-version, and the panel
gives the Anthropic SDK base URL. A grant that arrives through a group shows a notice instead,
because credentials are issued for direct grants only.
Use MCP servers to publish a registered streamable MCP server through a managed API Management gateway. MOSAIC creates a passthrough MCP API, a backend, an enforcement fragment, an API policy, and a per-publication protected-resource-metadata API. It owns those resources under the same reviewed plan, explicit apply boundary, and environment rules as model publishing. See Publish MCP servers through API Management.
Published MCP servers accept Entra runtime tokens only. People and agent users request
api://<model-runtime-client-id>/Mcp.Invoke. Applications, managed identities and agent identities
request api://<model-runtime-client-id>/.default and need Mcp.Invoke.Application. The gateway
checks direct grants first, then matching Entra security-group grants from the token's groups
claim, and applies call limits per direct caller or per group member. Token limits do not apply to
MCP servers.
The gateway strips caller credentials before forwarding and attaches its own managed identity when the registered MCP endpoint uses managed identity. It never passes the caller's bearer token or an APIM subscription key to the upstream server. API-key upstream MCP servers and SSE-only upstreams are not published in this phase.
Interactive clients discover sign-in through the gateway's WWW-Authenticate challenge and the
protected resource metadata document. A VS Code entry is just an HTTP MCP server with the published
URL:
{
"servers": {
"mosaic-example": {
"type": "http",
"url": "https://<gateway-host>/<api-path>/mcp"
}
}
}See Connect to MCP servers published through MOSAIC for people, agent identities, agent users, security groups and troubleshooting.
Write locks do not expire automatically: a timeout or a new API instance does not prove an old
worker or its submitted ARM operations have stopped. Check recovery status performs diagnostics
only. It first reads GET /api/v1/publications/{id}/lock, so it also works for a retained metadata
mutation without a publish-run record. A missing lock does not itself prove runtime access.
An administrator can diagnose the exact returned ownerId using:
POST /api/v1/publications/{id}/recover
{"runId":"<exact-owner-id>","confirmQuiesced":false}
Before submitting confirmQuiesced:true, the operator must stop the original worker and confirm
that every ARM operation it submitted has reached a terminal state. Do not infer this from a
restart, elapsed time, or an interrupted status. Confirmed recovery establishes denial where
needed, preserves ownership, and releases the lock only after durable recovery results. Refetch
the publication afterward and review a fresh plan; failure to establish safe state remains
unknown/locked. This is not an automatic retry, and the UI never sends that confirmation.
Automated unit tests and policy snapshots do not prove that a real gateway accepts a caller's
token or can reach its model. scripts\verify_model_access.py is an opt-in verification client:
it calls the actual APIM endpoint and does not proxy through MOSAIC, provision resources, change
grants, or save credentials.
Deploy the feature and bootstrap its runtime registration. Then publish the models to check, in a non-production environment, and apply grants for them: any number of user grants held by one person, and application grants held by one workload. The script reads each grant's connection details from MOSAIC and calls the operation its publication exposes:
- Azure OpenAI chat completions under
/openai/, with--api-version. - Foundry Models chat completions under
/models/, with--models-api-version. - Anthropic Messages at
/anthropic/v1/messages, which takes no API version.
Credentials come from process environment variables, not source files:
MOSAIC_SMOKE_USER_CONTROL_TOKEN: the granted user's token for the MOSAIC API, withUser. Needed for user grants and for grants held by someone else. For application grants, it also lets the script check that the user can't reveal the application's key.MOSAIC_SMOKE_ADMIN_CONTROL_TOKEN: an administrator's MOSAIC API token for application-key handoff. Needed for application, agent identity, and security-group grants, and to confirm whose grants the user must not reach.MOSAIC_SMOKE_USER_RUNTIME_TOKEN: that user's delegated model-runtime token. The user can get one by signing in with the model client, as in Call a published model with an Entra token.MOSAIC_SMOKE_APPLICATION_RUNTIME_TOKEN: the granted application's model-runtime token.MOSAIC_SMOKE_UNGRANTED_USER_RUNTIME_TOKEN: for--check-ungranted-user, a model-runtime token for a different user who holds none of the grants.MOSAIC_SMOKE_AGENT_RUNTIME_TOKEN: for--agent-entitlement, an app-only model-runtime token for the granted agent identity.MOSAIC_SMOKE_GROUP_MEMBER_RUNTIME_TOKEN: for--group-entitlement, a model-runtime token for a user, application or agent that is a member of the granted Entra security group.
The script can sign in instead of reading runtime tokens. With --user-token-source device-code
it uses the model client that MOSAIC names in the connection details, and prints a code for the
user to enter at the sign-in page. With --application-token-source client-credentials it reads
the workload's MOSAIC_SMOKE_APPLICATION_CLIENT_ID and MOSAIC_SMOKE_APPLICATION_CLIENT_SECRET.
Use a short-lived secret and delete it afterward.
python scripts\verify_model_access.py `
--api-base-url https://<mosaic-api-host> `
--gateway-origin https://<approved-apim-host> `
--user-entitlement <user-grant-id> `
--user-entitlement <another-user-grant-id> `
--application-entitlement <application-grant-id> `
--agent-entitlement <agent-grant-id> `
--group-entitlement <security-group-grant-id> `
--api-version <azure-openai-api-version> `
--models-api-version <foundry-models-api-version> `
--user-token-source device-code `
--check-ungranted-user `
--send-model-requestsThe explicit flag acknowledges actual inference requests and their consumption; each asks for at
most 8 output tokens. Every sign-in happens before the first model call. For each grant, the
gateway must first reject an anonymous call and an invalid key. Token validation must refuse a
MOSAIC control-plane token, and an invalid token sent with a valid key, with 401. When the run has
a token for the other kind of subject, the gateway must refuse it with the grant's key with 403,
because the two name different grants. With --check-ungranted-user, the grant lookup must also
refuse the ungranted user's token with 403. When Entra tokens are off, every token must get 401. A
rejection with the other status came from a different rule, so the check fails. Then the grant
must reach its model with its own key and its own Entra token, whichever methods are applied.
The script refuses redirects or an unexpected gateway origin. Before using a token, it checks the
token's audience, permission and expiry, and that Entra issued it in the version 2.0 format the
gateway accepts. The run fails if the grant subject's own token or the ungranted user's token
doesn't pass, or expires within a minute. A check that only borrows a token, such as the other
subject's token with this grant's key, is skipped with the reason instead, because token
validation would refuse that token before the rule under test. The script prints neither keys,
tokens, nor model output. Sign-in failures show only their error and AADSTS codes, which the
troubleshooting table explains. Chat
requests cap output with max_completion_tokens on Azure OpenAI routes and max_tokens on
Foundry Models routes; --chat-token-parameter overrides that for API versions that differ. Set
MOSAIC_SMOKE_PAYLOAD to a bounded request JSON object to send instead of the default; the script
still sets its model to each grant's deployment.
--agent-entitlement and --group-entitlement each name a grant to check with the
administrator's token, after the user and application grants. Repeat either flag for each such
grant. An agent identity grant's connection details must name an agent identity, a .default
runtime scope, the Models.Invoke.Application app role, and Entra tokens applied with nothing
pending. A security-group grant must report that it has no keys, and MOSAIC must refuse to reveal
one with 409. The script warns when the group member's token has no groups claim or signals
group overage, because the gateway can't then match the caller to the group. Then each grant must
reach its model with MOSAIC_SMOKE_AGENT_RUNTIME_TOKEN or
MOSAIC_SMOKE_GROUP_MEMBER_RUNTIME_TOKEN, which the script reads rather than signing in. These
grants don't take part in the rejection checks, the proofs, or --watch-revocation.
Each run can add one proof, using fresh isolated grants with no other callers. Only the gateway's own limit counts: a 429 from the model deployment, or from a different limit, fails the proof as inconclusive.
--prove-shared-budget: for grants limited to 2 requests per 300 seconds, with keys and Entra tokens applied. Two successful calls using the primary key and Entra token must exhaust the budget for the secondary key too, which then gets the gateway's call-limit 429.--prove-token-limit: for grants limited to at most 100 tokens per minute, without call limits. The model's own token limit for each grant must be higher than the grant's. Further calls must reach the gateway's token-limit 429 withRetry-After. Classic-tier gateways can't limit an Anthropic model's tokens, so this proof doesn't apply to Claude there, and the script refuses it before calling the model.
--watch-revocation <grant-id> ends the run by waiting while you revoke that grant in the console,
which disables it, and apply its model's access plan. Don't delete the grant: the script follows
its status in MOSAIC. Every apply briefly refuses all calls to the model, so rejections count only
after MOSAIC reports the grant revoked. Then the gateway must reject the grant's key, and its Entra
token with 403 from the grant lookup (401 if the plan also turned Entra tokens off), twice in a
row. The watch polls every --revocation-interval seconds (30 by default) and fails after
--revocation-timeout seconds (900 by default). The tokens it uses must stay valid for the whole
watch plus a minute; otherwise it stops before waiting.
--foreign-user-entitlement <grant-id> checks that the user can't reach a grant someone else
holds. Repeat it for each such grant. With the administrator's token, the script first confirms
that the grant exists and that a different user holds it, so a mistyped ID or one of the user's
own grants can't pass as a refusal. Then, with the user's token, MOSAIC must leave the grant out
of the user's lists, including the portal's My access, and out of the user's 90-day usage
report, both its rows and its timeline. It must also refuse the grant's connection details and
its key with 403 or 404. A MOSAIC from before the usage report (ADR 0015) answers its route with
404, and the script says it skipped that part. These checks call only MOSAIC's API, before any
model call, and each refused key request is recorded in MOSAIC's audit log. A run that names only
grants held by someone else sends no model requests, so it doesn't need --send-model-requests.
Separately exercise method toggles through reviewed plans, allowing APIM to propagate each change before rerunning the script. Verify rotation by changing a test subscription key directly in APIM and revealing it again: no MOSAIC synchronization should be needed. Do not report these live scenarios as passed when deployment, consent, credentials, or a test gateway are unavailable.
e2e/ holds a live, human-in-the-loop Playwright harness. People sign in their own test
accounts, and the harness drives the web console and portal to:
- Import Azure OpenAI and Foundry endpoints, publish their models, and grant them.
- Check that each end user or workload can call its model, and that others are denied.
The roadmap tracks the phases and the journey matrix. The
runbook covers setup, personas, flags and secret hygiene. Its verify
command runs scripts\verify_model_access.py with the personas' own MOSAIC API tokens, and
enters its device codes in their browsers.
Every gateway, model endpoint, and registered MCP server belongs to one environment, or is Unclassified. Administrators manage environments in Settings → Environments. MOSAIC seeds Development, Test, QC, Staging, Production, and Sandbox. Administrators can rename or recolor them and add their own.
Each environment has:
- a key that never changes;
- optionally, the production-class flag;
- optionally, a list of other environments whose endpoints its gateways may also front.
A production-class environment may list only other production-class environments.
Whenever a model or MCP server is published, MOSAIC judges the pairing of the gateway with the model endpoint or MCP server:
- Allowed: both are in the same environment, or the gateway's environment lists the endpoint's as an exception.
- Blocked: two different classified environments without an exception.
- Blocked: a production-class resource paired with an unclassified one, in either direction.
- Warning: any other pairing that involves an unclassified resource. Once Require classification is on, these are blocked too.
The publish dialogs disable blocked deployments and MCP servers and say why. Creating, planning, and applying a publication each check again. A plan also records the verdict it was reviewed under. If either environment, a production-class flag, the exception, or Require classification changes before apply, apply asks for a fresh plan.
Classifying resources
- Registration asks for an environment.
- To change it later, use Change environment on the resource's page. To classify many
resources at once, use the Unclassified card in Settings. Both go through
POST /environment-assignments. The gateway, model endpoint, and MCP server update routes don't accept an environment, so every change passes the same checks. - MOSAIC suggests an environment, but never applies one without confirmation. It suggests from
either:
- an Azure
environmentorenvtag it read during preflight or the subscription scan; or - the legacy environment label.
- an Azure
- MCP servers are registered by URL, so they have no Azure tag.
Changes that are refused
MOSAIC refuses any change that would leave an applied model or MCP publication blocked:
- re-classifying a gateway, model endpoint, or MCP server;
- editing or deleting an environment;
- turning on Require classification.
The refusal names the publications. Sometimes a gateway must move together with its endpoints or MCP servers, for example to classify an unclassified pair as Production. The console then submits them as one batch, which MOSAIC validates as a whole and writes atomically.
Grants follow their resource. Moving a resource with enabled grants into or out of a production-class environment lists the people and applications affected, and requires confirmation. The audit event records the grants.
In the portal
- Every catalog entry and grant shows its environment, and the catalog can be filtered by it.
- People request each environment separately, so development access doesn't imply production access.
- A request records the environment it was made for. If the resource has moved since, approval asks the administrator to confirm the new environment.
Findings point out blocked pairings that MOSAIC's rules didn't stop. They cover:
- an applied model or MCP publication whose pairing is blocked, which only a change made outside MOSAIC can cause;
- a gateway backend or API that calls a registered model endpoint in an incompatible environment;
- a gateway MCP server whose URL is a registered MCP endpoint's URL, unless MOSAIC published it.
Findings are advisory, and each shows its evidence and confidence. They appear on the gateway, in Settings, and in the import dialog. MOSAIC doesn't inspect backends referenced only from policy.
| Method | Route under /api/v1 |
Result |
|---|---|---|
| GET | /environment-catalog |
Environments with usage counts, the compatibility matrix, and Require classification |
| POST | /environment-catalog/environments |
Add an environment |
| PATCH, DELETE | /environment-catalog/environments/{key} |
Edit or delete an environment |
| PATCH | /environment-catalog/settings |
Turn Require classification on or off |
| GET | /environment-suggestions |
Unclassified resources and the environment suggested for each |
| POST | /environment-assignments |
Classify or re-classify resources as one validated batch |
| GET | /environment-findings?gatewayId= |
Advisory findings, optionally for one gateway |
| GET | /portal/environments |
The environments, for portal users |
ADR 0014 records these rules.
The portal's Usage & cost page shows a person the requests, tokens, and estimated cost of each
grant they hold. It breaks them down by day, by environment, and by resource, and shows each
quota's utilization within that quota's own window. It reads
GET /api/v1/me/usage?period=7d|30d|90d, which returns only the caller's own usage.
Until MOSAIC reads Log Analytics, the report is simulated:
- The figures are deterministic, built from the caller's real grants and limits, and never exceed a quota. A given day shows the same figures whichever period is selected.
- A disabled grant shows no usage.
- The page is labeled Sample data.
- Costs are estimates at illustrative rates, only for models MOSAIC knows, and never a bill.
Once a real source is configured, a failure is reported, never replaced with simulated data. See ADR 0015.
The Usage tracking column says how each grant's real usage will be found:
- At the gateway: a model or MCP publication MOSAIC applied tags every call it authorizes with the grant it matched. This links Entra-token, security-group, and MCP grants, which have no APIM subscription. A security-group grant's tag also carries the caller's object ID, so each member sees only their own calls.
- By APIM subscription: the grant's binding names the subscription that carries its calls. Imported model APIs, imported MCP servers, products, and model deployments can be linked only this way.
- Not linked yet: nothing links the grant, so its real usage will show as unattributed. A publication applied before gateway tagging existed starts tagging on its next apply.
The tag is an API Management trace with source mosaic at information severity, emitted before
any limit, so throttled calls are tagged too. Its message reads
mosaic-attribution v=1 g=<grant> m=<object ID>, where m is empty unless a security-group grant
matched. For it to reach Log Analytics, set the gateway's Azure Monitor diagnostic verbosity to
Information or Verbose. ApiManagementGatewayLogs then records it in TraceRecords. The bootstrap
gateway's Application Insights diagnostic logs at Information, so each governed call also adds one
trace there, whatever the sampling rate. To stop them, set that diagnostic's verbosity to Error. A
security-group tag includes an Entra object ID, which is personal data, so apply your retention and
access rules to both destinations.
For published MCP servers, scripts\verify_mcp_access.py checks the gateway's protected resource
metadata flow and, when supplied, denied and granted runtime tokens. It never calls an MCP tool.
Prepare a non-production published MCP server, then provide any optional tokens through environment
variables:
MOSAIC_SMOKE_MCP_DENIED_RUNTIME_TOKEN: optional, a runtime token that hasMcp.InvokeorMcp.Invoke.Applicationbut no applied MCP grant.MOSAIC_SMOKE_MCP_GRANTED_RUNTIME_TOKEN: optional, a runtime token with an applied MCP grant.
python scripts\verify_mcp_access.py `
--server-url https://<approved-apim-host>/<api-path>/mcp `
--tenant-id <tenant-id> `
--runtime-client-id <model-runtime-client-id> `
--check-denied-token `
--check-granted-tokenThe verifier confirms that an unauthenticated request receives a 401 with resource_metadata,
that the metadata JSON names the server URL, tenant authorization server and
api://<runtime-client-id>/Mcp.Invoke, that an ungranted token is denied with
insufficient_scope, and that a granted token completes MCP initialize over streamable HTTP. Do
not report live MCP interoperability as passed when the APIM preview contract, consent, credentials
or a test server are unavailable.
The API contains a deterministic policy preview using current documented policies:
authentication-managed-identityset-backend-servicellm-token-limitllm-emit-token-metricvalidate-azure-ad-token, explicit grant authorization,rate-limit-by-key, andquota-by-keyfor opted-in governed model and MCP access
The preview and the publish plan both return the same plain-language facets used for observed policy, plus a content digest. Generated XML stays in process and is never serialised to a caller, so MOSAIC-authored markup never reaches a browser any more than customer-authored markup does.
Nothing detects drift in the background yet. Re-plan on the Models page makes a fresh plan and opens it for review, and nothing in API Management changes until the administrator chooses Apply plan. A plan compares what exists, not what it contains: a resource missing from API Management shows as Create, and one someone changed shows as Update, the same as one nobody touched, because applying replaces it with what the publication describes. This is the same gap ADR 0005 already acknowledged for imported records.
- Foundation: secure deployment, domain, directory CRUD, runtime configuration, observability wiring, typed APIM/Foundry/reconciliation boundaries.
- Gateway onboarding: multi-gateway registry, access verification with guided remediation, full inventory synchronisation, AI surface detection, and plain-language policy.
- Model and MCP onboarding: discover MCP servers, detect model-fronting APIs across Azure and third-party providers and import a chosen selection into desired state, register Azure OpenAI and Foundry endpoints to enumerate their deployed models and verify each gateway's runtime access to them, and register MCP servers directly to record the tools they declare.
- Model publishing: expose an observed deployment through a gateway by writing its backend, policy fragment, API, operations, product and subscription, through a deterministic plan, an explicit apply, per-step results, and rollback that removes only what it created. This is the orchestration ADR 0009 defers to, for models.
- Governed model access: direct user/application/agent grants become APIM
subscriptions and/or Entra authorization, and Entra security-group grants become token-only APIM
authorization, with shared limits, explicit apply/revoke, trusted
orchestratedbindings, and on-demand key retrieval for direct grants. Approving an access request creates the requester's grant intent but does not apply it. The end-user portal now provides My access (including connection details and on-demand key reveal for applied direct model grants), catalog, and access-request screens, gated by theUserapp role and themosaic-<env>-portalregistration; see ADR 0008. - MCP publishing and enforcement: publish registered streamable MCP servers through managed gateways with a passthrough MCP API, per-publication resource metadata, Entra-only grants, call limits and fail-closed recovery; see ADR 0017.
- Insights and chargeback: Azure Monitor queries over
ApiManagementGatewayLogsandApiManagementGatewayLlmLog, consumption measured against each entitlement's own enforcement window, per-user attribution, token/traffic/cost allocation, budgets, and portal usage views alongside administrator dashboards. The portal's Usage & cost page and its/me/usagecontract already exist on simulated data, and governed calls are already tagged with their grant at the gateway; this phase supplies measured figures. - Catalog ecosystem: API Center experiences, MCP tool-level governance, broader self-service workflows, and environment chains that relate the same model across environments and clouds.
- Production hardening: private networking, multi-region/production APIM tiers, CMK where required, measured partition scaling, retention and operational SLOs.
See the architecture decisions for the durable rationale behind this foundation.






















