Skip to content

Repository files navigation

MOSAIC

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.

Screenshots

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.

Light and dark

Both apps follow the operating system's light or dark setting, and the console can pin either one under Settings > Appearance.

Light Dark
The console overview in the light theme The console overview in the dark theme
Console overview. Live counts of the people, agents, applications and security groups, and MOSAIC groups registered in MOSAIC, each opening its tab under Identity. An Environments card counts the gateways, model endpoints, and MCP servers in each environment. Below them, the telemetry, cost, model-ranking, and service-health panels show labelled sample data until MOSAIC queries Azure Monitor.
The portal catalog in the light theme The portal catalog in the dark theme
Portal catalog. The model APIs and MCP servers published to portal users, each labelled with its environment and, for an MCP server, whether the gateway enforces it. People request the development or production copy with an optional justification, and can see what they already hold or have asked for.

Administrator console

Registered gateways with their environment, status, AI API counts, and last sync

Gateways. Existing API Management services registered by resource ID, each in one environment. MOSAIC reports whether it can read each one, how many APIs front Azure AI backends, and when it last synced.

A gateway overview with inventory counts and service details

Gateway overview. One gateway at a glance: its AI APIs, operations, MCP servers, access paths, and policy rules, with its environment, mode, and service details. Tabs open its APIs, MCP servers, products, subscriptions, users and groups, policies, and backends.

A gateway's APIs described in plain language with their operations

APIs and endpoints. Each API on a gateway described in plain language: the backend it points at, what it exposes, the policies that govern it, and its operations.

Published models with status, gateway, API path, and actions

Models. Model deployments MOSAIC has planned or published into API Management, with their gateway and API path. Publishing changes APIM only when a reviewed plan is applied, and only on a gateway in manage mode.

Registered and published MCP servers with environment, status, authentication, and tools

MCP servers. Servers registered directly or imported from a gateway, with their environment, connection status, authentication method, and tools, and the servers MOSAIC publishes, with their gateway apply state. MOSAIC reads what a server offers and never calls a tool.

The Identity page's Agents tab listing agent identities and an agent user, with a detail panel

Identity. The people, agents, applications, and security groups MOSAIC references by Entra object ID, each on its own tab. The Agents tab shows each agent's blueprint or parent agent; Entra stays the source of truth, and MOSAIC keeps only a local label and type.

The Add agent dialog with agent search results and already-added badges

Directory picker. Add agent on the Agents tab searches Microsoft Entra for agents, and the same picker finds people and security groups. Results show what is already in MOSAIC and what can still be added.

A security group detail page with members loaded from Microsoft Graph

Security group members. A recorded Entra security group shows its read-only Microsoft Graph member list. MOSAIC can show which members are also recorded locally for direct grants.

Grants with subjects, resources, environments, limits, desired and applied state, and bindings

Entitlements. Grants of model APIs and MCP servers to people, agents, applications, MOSAIC groups, and Entra security groups, with each grant's environment, limits, desired and applied state, and APIM binding. Gateway enforcement changes only through a reviewed apply.

Overlapping grants showing which grant applies, each grant's limits, and links to the grants

Overlapping grants. MOSAIC explains when multiple grants can reach the same caller on the same resource, with each grant's limits and a link to it. Direct grants win over group grants, and the most generous security-group grant wins among groups.

MCP publish review with access changes and plan steps

MCP publish review. A MOSAIC-owned MCP server is published through API Management only after the administrator reviews the access snapshot and the exact gateway resources that will change.

Analytics with request, token, success-rate, and cost summaries

Analytics. A preview of the usage, token, cost, and chargeback views, filtered by time range, model, and environment. It shows labelled sample data until MOSAIC queries Azure Monitor and Log Analytics.

The environment catalog in Settings with usage counts and one unclassified resource

Environments. The catalog administrators define in Settings: the built-in environments plus custom ones such as Partner, each with a color and a production-class flag. Gateways, model endpoints, and MCP servers are each classified into one, and any not yet classified wait below with suggestions to review.

Findings where a development gateway routes to production endpoints

Environment findings. Pairings already in API Management that break the environment rules, such as a development gateway routing to production endpoints, with the evidence and how confident the match is. Findings are advisory and never block anything.

End-user portal

A model grant with its environment, limits, and expanded connection details

My access. Each grant with its environment, limits, APIM state, and how its usage is tracked. A direct model grant expands to show its endpoint, operations, accepted credentials, and Entra details, with code samples and an on-demand key reveal below.

An enforced MCP grant with VS Code mcp.json connection details

MCP connection. An enforced MCP grant expands to show the server URL, protected resource metadata, VS Code mcp.json snippet, Entra authentication values, and call limits.

Approved, denied, and pending access requests with their environments

My requests. Access requests with their environment, justification, state, and the administrator's decision note. A pending request can be withdrawn.

Usage and cost totals with a daily trend per environment

Usage and cost. A person's requests, tokens, and estimated cost across everything they hold, with a daily trend per environment. It shows labelled sample data, simulated from their real grants and limits, until MOSAIC queries Log Analytics.

Usage by resource with each grant's environment, quotas, rate limits, and usage tracking

Usage by resource. The same figures by environment and by granted resource, with each grant's quotas, rate limits, and usage tracking: at the gateway, by APIM subscription, or not linked yet.

Architecture and trust boundaries

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
Loading

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.

What is implemented

  • Python 3.12 FastAPI API with OpenAPI, structured JSON logging, Azure Monitor OpenTelemetry, correlation IDs, anonymous /healthz and dependency-aware /readyz
  • Entra issuer, audience, signature, tenant, expiry, and algorithm validation, with app-role authorization decided per route: Admin for every administrative route, User for 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 Admin role before it shows any of that; a caller with only User, 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 User app 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 environment tags 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/usage contract 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
  • azd and 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 azd hooks

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.

Prerequisites

  • 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.All and AgentIdentity.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.

Local development

Install dependencies:

uv sync --all-packages --group dev
Set-Location apps\web
npm ci

Run 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-api

Local 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 dev

apps/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.

Quality checks

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_nginx

Deploy with azd

Sign 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 up

The preprovision hook idempotently creates separate single-tenant Entra registrations:

  • mosaic-dev-api: access_as_user delegated scope, plus an Admin and a User app role
  • mosaic-dev-spa: administrator console SPA redirects and delegated permission to the API
  • mosaic-dev-portal: end-user portal SPA redirects and delegated permission to the API
  • mosaic-dev-model-runtime: the separate audience for APIM model calls, with the Models.Invoke delegated scope, Models.Invoke.Application application permission, Mcp.Invoke delegated scope and Mcp.Invoke.Application application permission. Bootstrap configures groupMembershipClaims: SecurityGroup so 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 delegated Models.Invoke and Mcp.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 --purge

Data model and Cosmos layout

Every 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 readiness
  • ModelEndpointSyncRun: the outcome of one model discovery run
  • CatalogModel: provider model identity/version
  • ModelDeployment: callable deployed endpoint
  • Principal (users, applications, managed identities, agent identities, agent users and Entra security groups), Group, GroupMembership
  • EnvironmentCatalog: the tenant's environments, which are production-class, which other environments each one's gateways also accept endpoints from, and whether classification is required
  • Gateway: a registered API Management service, its verified access, and its inventory summary
  • GatewaySyncRun: the outcome of one inventory synchronisation
  • ModelApi: an API Management API an administrator adopted as a governed model endpoint
  • McpServer: an API Management MCP server an administrator adopted
  • McpEndpoint: a registered MCP server MOSAIC connects to and reads tools from
  • Publication: intent to expose one model deployment through one gateway, plus the API Management resources an apply created and whether MOSAIC created each one
  • PublishPlan, PublishRun: the reviewed changes and the audited result of applying them
  • Entitlement: 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 gateway
  • AccessRequest: a portal user's request for a resource they can see but are not entitled to
  • CredentialReference: Key Vault secret URI only
  • PolicyRevision, SyncOperation, AuditEvent Cosmos 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.

Security model

  • 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_admin demands the Admin role and guards every administrative route; require_portal_user demands the User role, which Admin also satisfies. Neither role is implied by tenant membership, so an operator assigns User — usually to an Entra group — before anyone can use the portal.
  • require_mosaic_role admits either role and guards only GET /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 for Admin; for User alone, a page saying the console is for MOSAIC administrators and that User opens 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 uses ManagedIdentityCredential.
  • 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.All and AgentIdentity.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 groups claim against an applied security-group grant; being signed into MOSAIC or holding its Admin role does not itself grant model access.
  • Entra security-group grants are authorized only from the runtime token's groups claim. 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.Application app 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.Invoke on 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 setting MOSAIC_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 no listKeys permission 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.

Gateways

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.

MCP servers

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".

Importing models and MCP servers

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.

Registering MCP servers

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.

Policies without markup

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.com with 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.

Model endpoints

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 dataActions minus notDataActions against 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.com host and the https://ai.azure.com token 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 Reader first 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 create command. 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 disableLocalAuth from accounts where it was never set, and its default is false, 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, and details carries its id and name.
  • 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 (accessState unknown);
  • 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 models

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.

Governed model access

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.

Published MCP server access

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.

Recovering an interrupted operation

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.

Verifying a real gateway

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, with User. 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-requests

The 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 with Retry-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.

End-to-end UI testing

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.

Environments

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 environment or env tag it read during preflight or the subscription scan; or
    • the legacy environment label.
  • 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.

Usage and cost in the portal

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 has Mcp.Invoke or Mcp.Invoke.Application but 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-token

The 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.

Reconciliation boundary

The API contains a deterministic policy preview using current documented policies:

  • authentication-managed-identity
  • set-backend-service
  • llm-token-limit
  • llm-emit-token-metric
  • validate-azure-ad-token, explicit grant authorization, rate-limit-by-key, and quota-by-key for 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.

Roadmap

  1. Foundation: secure deployment, domain, directory CRUD, runtime configuration, observability wiring, typed APIM/Foundry/reconciliation boundaries.
  2. Gateway onboarding: multi-gateway registry, access verification with guided remediation, full inventory synchronisation, AI surface detection, and plain-language policy.
  3. 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.
  4. 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.
  5. 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 orchestrated bindings, 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 the User app role and the mosaic-<env>-portal registration; see ADR 0008.
  6. 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.
  7. Insights and chargeback: Azure Monitor queries over ApiManagementGatewayLogs and ApiManagementGatewayLlmLog, 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/usage contract already exist on simulated data, and governed calls are already tagged with their grant at the gateway; this phase supplies measured figures.
  8. 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.
  9. 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.

About

Model Orchestration, Stewardship, Allocation, Insights, and Chargeback

Resources

Code of conduct

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages