Skip to content

APIM parity ledger

This is the live top-level ledger. Detailed generated operation, policy, portal-workflow, protocol and gateway matrices will be added as implementation inventories are generated.

Snapshot date: 2026-08-19

The date is when the grades were last re-verified, not merely edited: the Go suite green at 100% statement coverage, go vet clean, and scripts/check_witnesses.py --strict confirming every row graded implemented or sdk-verified names a witness that still exists. Re-stamp it only after doing that, or it becomes a claim of its own that nobody checked.

Every row graded implemented or sdk-verified names its witness in docs/witnesses.json, and scripts/check_witnesses.py --strict fails CI if a witness stops existing — a green row whose evidence was renamed away is a claim the reader cannot audit. Rows graded partial or planned are exempt: they claim nothing yet.

Capability trackStateTarget phaseVerification witness
process/config/TLS/store/clockimplementedP0Go tests, GoReleaser matrix, container and Compose smoke tests
service ARM lifecycle and LROspartialP0lossless 2024-05-01 schema inventory, GET/PUT/PATCH/DELETE/list, completed LRO, 100% local statement coverage, and read-only differential harness; authorized Azure evidence pending
APIs/operations/schemas/tags/products/groups/users/subscriptions/named values/backends/caches/certificates/policy fragments/loggers/diagnosticspartialP0-P1Core lifecycles and four-SDK secret rotation; Go SDK revision cloning including lossless schemas, API/operation tag associations, and API diagnostics; tags and links, groups and memberships, user CRUD/SSO/tokens, release promotion, version-set, named-value, backend, cache, identity-provider, OpenID Connect provider, OAuth authorization server, documentation, PFX certificate, logger, and service/API diagnostic CRUD; JavaScript SDK fragment lifecycle/reference evidence plus recursive gateway expansion and cycle/missing-reference validation; live Key Vault secret retrieval with last-known-good status and 401 Bearer challenge token retry; broader resources pending
stable 2024-05-01 operation inventorypartialP1-P7The published operation surface, measured rather than described. All 611 operations Microsoft declares for stable 2024-05-01 are enumerated in docs/generated/operations-2024-05-01.json from a pinned spec commit, and every one of them is probed against a fresh service by e2e/inventory: 367 routed, 124 absent, 120 unmeasured. routed means the operation answers something other than 404, which proves it exists and says NOTHING about whether its behaviour is right — it is a floor under the surface, not a parity claim, and the rows above are where behaviour is graded. absent is a 404 that a missing resource cannot explain, because the probe created what it asked for first, or because Microsoft declares a create for that path and the emulator 404s it too. unmeasured is the honest remainder: a 404 this harness cannot attribute, mostly resources three levels deep whose parents it cannot synthesise a valid body for. The first published figures (294/166/151) were wrong about 12 operations, and the harness was at fault, not the emulator: it seeded resources at service scope only and then substituted those names into WORKSPACE paths, so a 404 for a missing parent was read as a missing route. Nine operations reported absent are served, and three were not conclusive at all. A seeded parameter is only seeded in the scope it was created in, and the verdict rules cannot see scope. Deliberately NOT claimed: response shape, paging, ETag and LRO behaviour on any of the 294, the 2025-09-01-preview surface, and the data plane, none of which this measures.
ARM conditional requestsimplementedP1central strong/weak ETag parser, If-Match/If-None-Match/wildcard behavior, atomic mutation checks, stable 2024-05-01 required-header inventory for implemented PATCH/entity DELETE routes with association exclusions, malformed/missing-header and 304 tests, and official Go SDK update/delete plus stale-update 412 PreconditionFailed witnesses; exact Azure error wording and broader-operation differential fixtures pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
ARM collection queriesimplementedP1central stable paging, filtered total count, absolute query-preserving nextLink, int32 validation, comparison/logical/string-function OData grammar, stable scalar field/operator/function contracts for every implemented collection shape, named-value tags/any(...) and tags/all(...), selector availability/boolean validation, association-backed product tag filtering, tag scope filtering, Key Vault refresh-failure projection, API/version-set/tag/product/user expansion projections, undocumented OData-option rejection, stable name asc|desc policy-fragment ordering, exact failure coverage, official Go SDK filtered multipage pager, and JavaScript SDK filtering/ordering/tag-predicate witnesses; Azure differential fixtures pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
ARM error envelopeimplementedP1canonical body code/message/target, validation error.details field entries, x-ms-error-code, request/correlation IDs, exact HTTP coverage, and official Go SDK ResponseError witness; exact Azure wording and differential fixtures pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
official management SDKssdk-verifiedP0-P1Go v1.1.1, JavaScript 10.0.0, Python 5.0.0, and .NET 1.3.1 service/API/operation/subscription plus protected-gateway workflows
preview 2025-09-01-previewplannedP8isolated preview suite
HTTP gateway and routingimplementedP0-P1operation template, subscription, custom-host routing, backend TLS validation/client certificates, retry policy execution with body replay/status predicates, persisted backend circuit-breaker rules, SSE flushing, query-parameter/variable/body/method mutations, CORS response/preflight handling, backend recorder integration test
OpenAPI import/exportpartialP1/P5transactional OpenAPI 2.0 and 3.x JSON/YAML inline and linked import, path and operation metadata validation, retained source and revision cloning, deterministic operations/schema projection, signed five-minute OpenAPI 3/Swagger 2 export links, official Go APIClient/APIOperationClient/APIExportClient plus gateway integration; broader OpenAPI semantic validation, policy/representation import, WADL/WSDL/GraphQL/gRPC formats, and Azure differential fixtures pending
canonical API ARM documentsimplementedP1migration-safe companion persistence, lossless unknown-property PUT/GET/list, recursive PATCH with null deletion, sanitized import fields, revision inheritance, transaction rollback tests, exact aggregate coverage, and official Go SDK description create/update round-trip; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical operation ARM documentsimplementedP1migration-safe companion persistence across direct writes, OpenAPI import, and revision cloning; lossless request/response/parameter metadata, recursive PATCH null deletion, rollback tests, exact aggregate coverage, and official Go SDK description/clone round-trip; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical API-schema ARM documentsimplementedP1migration-safe transactional envelope persistence independent of nested schema content, lossless unknown-property PUT/GET/list, OpenAPI import and revision inheritance, read-only identity repair, rollback/error tests, exact aggregate coverage, and official Go SDK components-document witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical product ARM documentsimplementedP1migration-safe companion persistence, lossless description/terms/subscription settings, recursive PATCH null deletion, correct notPublished default, rollback tests, exact aggregate coverage, and official Go SDK round-trip/default-state witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical group ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list and product/user association projections, recursive PATCH with nullable-field clearing, rollback tests, exact aggregate coverage, and official Go SDK create/update/get/list witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical API version-set ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list, recursive PATCH with optional-field clearing, rollback tests, exact aggregate coverage, and official Go SDK update/get description witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical tag ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list and API/operation/product association projections, recursive PATCH, rollback tests, exact aggregate coverage, and official Go SDK lifecycle/association witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical policy-fragment ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT replacement and GET/list projection, authoritative validated XML/format/provisioning fields, rollback tests, exact aggregate coverage, and official JavaScript SDK lifecycle/reference witness; remaining resource families and Azure differential fixture pending
canonical API-release ARM documentsimplementedP1migration-safe companion persistence within atomic revision promotion, lossless unknown-property PUT/GET/list, recursive PATCH with nullable-note clearing, authoritative target/timestamps, rollback tests, exact aggregate coverage, and official Go SDK update/get witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical user ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list and group-member projections, recursive PATCH/null clearing, handler/store/wire password and key redaction, rollback tests, exact aggregate coverage, and official Go SDK lifecycle/update/get/SSO/token witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical subscription ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list/runtime projection, recursive PATCH/null handling, handler/store/normal-wire key redaction, explicit secret listing and independent rotation, rollback tests, exact aggregate coverage, and official Go SDK GET/listSecrets/rotation/gateway witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical named-value ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property PUT/GET/list, recursive PATCH and Key Vault/null handling, handler/store/normal-wire value redaction, explicit listValue, live secret retrieval with last-known-good lastStatus, rollback tests, exact aggregate coverage, and official Go SDK redacted-GET/update/listValue/gateway witness; remaining resource families and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical backend ARM documentsimplementedP1atomic primary-row lossless persistence, unknown-property and credential/TLS PUT/GET/list, recursive PATCH/null synchronization, legacy fallback, marshal-error tests, exact aggregate coverage, gateway certificate-reference evidence, and official Go SDK credential-preserving update/get witness; broader backend runtime behavior, remaining resource families, and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical cache ARM documentsimplementedP1lossless unknown-property PUT/GET/list persistence, handler/store connection-string stripping, deterministic {{Cache-ConnectionString-...}} wire references with existing-reference preservation, recursive PATCH with nullable description/resourceId clearing, useFromLocation default normalization, stable name/description/region collection filters, exact aggregate coverage, and official Go SDK create/get/list/update/delete witness; live Redis connectivity and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical identity-provider ARM documentsimplementedP1lossless unknown-property PUT/GET/list persistence, canonical facebook/google/microsoft/twitter/aad/aadB2C names, handler/store clientSecret stripping, GET omission with explicit listSecrets, recursive PATCH with nullable authority/tenant/policy/allowedTenants clearing, empty collection-filter contract, exact aggregate coverage, and official Go SDK create/get/list/listSecrets/update/delete witness; developer-portal sign-in and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical OpenID Connect provider ARM documentsimplementedP1lossless unknown-property PUT/GET/list persistence, handler/store clientSecret stripping, GET omission with explicit listSecrets, optional secret on create, recursive PATCH with nullable description clearing, case-preserving updates, stable name/displayName collection filters, exact aggregate coverage, and official Go SDK create/get/list/listSecrets/update/delete witness; policy/OIDC runtime use and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical authorization-server ARM documentsimplementedP1lossless unknown-property PUT/GET/list persistence, handler/store clientSecret stripping, GET omission with explicit listSecrets for client and resource-owner secrets, required grant-type enum validation, GET-required authorization methods, recursive PATCH with nullable optional-field clearing, case-preserving updates, stable name/displayName collection filters, exact aggregate coverage, and official Go SDK create/get/list/listSecrets/update/delete witness; OAuth policy runtime use and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical documentation ARM documentsimplementedP1lossless unknown-property PUT/GET/list persistence, required title with optional markdown content, documentationId length/pattern validation, recursive PATCH with nullable content clearing, case-preserving updates, stable name eq/contains collection filters, exact aggregate coverage, and Go handler/store lifecycle witnesses; official Go SDK client and Azure differential fixture pending
canonical certificate ARM documentsimplementedP1migration-safe transactional companion persistence, lossless unknown-property and Key Vault metadata PUT/GET/list/refresh, handler/store/wire PFX-data/password redaction, authoritative parsed subject/thumbprint/expiration, live PFX secret retrieval with last-known-good lastStatus, rollback tests, exact aggregate coverage, gateway client-certificate evidence, and official Go SDK PFX plus Key Vault refresh/get witness; remaining resource families and Azure differential fixture pending
Key Vault secret retrievalimplementedP1versioned and versionless secret-identifier GET against a Key Vault data-plane endpoint, 401 Bearer challenge token acquisition with a single authenticated retry, named-value and certificate PUT/refresh resolution, classified lastStatus codes, last-known-good values on failure, isKeyVaultRefreshFailed projection from last status, exact HTTP/store/handler coverage; Azure differential fixture pending
canonical logger ARM documentsimplementedP1lossless unknown-property PUT/PATCH/GET/list persistence, handler/store duplicate-credential stripping, deterministic normal-wire credential references with existing-reference preservation, logger-use protection, exact aggregate coverage, and official Go SDK non-disclosure witness; exact Azure reference naming and external sinks pending differential fixtures; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
canonical diagnostic ARM documentsimplementedP1atomic service/API-scope lossless persistence, recursive complex-setting PATCH, indexed explicit-null/default synchronization, authoritative identity/type projection, logger validation and revision inheritance, exact aggregate coverage, official Go SDK correlation/operation-name/pipeline/sampling witness, opt-in request/response header and body capture with byte limits and secret masking; full telemetry schema and Azure differential fixture pending; Also witnessed end to end by Microsoft’s packaged @azure/arm-apimanagement in CI (arm-documents-witness), which round-trips the family and checks its id/name/type and list projection
policy XML/inheritancepartialP0-P2Service-to-API policy fallback, API <base/> composition, operation-level policy composition, and product subscription composition using the matching operation plan, ARM GET/PUT for service/API/operation/product policies, API-scope XML round-trip/compiler, service-scoped {{named-value}} substitution including secrets, last-known-good activation, structured traces, query-parameter/variable/body/method mutations, CORS, check-header, validate-jwt through the Entra validator or an openid-config discovery endpoint, with audience/issuer/required-claim payload checks, a required exp honouring require-expiration-time and clock-skew, and signing keys cached from the endpoint, ip-filter, injected send-request, deterministic rate-limit/quota and keyed windows with nested api/operation children and quota bandwidth, keyed limit-concurrency slots, first-class sequential wait, response/value cache lookup/store/removal with gateway TTL integration, validate-status-code, validate-content, validate-headers, validate-parameters, validate-client-certificate, choose control flow, trace events, authentication-basic, authentication-managed-identity, authentication-oauth2, authentication-certificate, find-and-replace, json-to-xml, xml-to-json, jsonp, retry, standalone set-status, mock-response including example bodies, compile-time include-fragment expansion, fire-and-forget send-one-way-request, configured-validator validate-azure-ad-token with client-application-ids and required-claims, Adobe cross-domain XML, and gateway/backend redirect-content-urls; schema mock bodies, inline token-value, remaining families, and workspace inheritance pending
policy inventorypartialP1-P2classified Microsoft Learn catalog plus base/authentication-oauth2, per-entry sections DERIVED from the vendored reference pages’ Policy sections: line and cross-checked against Microsoft’s published snippets, compiler rejection, in every mode, of a policy in a section its page does not name, hand-written gateway/expression-field hints, and CI unclassified/compiler-drift/derivation gates; XML schema, remaining expression-bearing fields, gateway hints, and Azure differential remain
operator portalimplementedP1embedded status/snapshot/parity dashboard, trace navigation, controllable clock surface, persisted service/API/core-resource summaries, API/product/backend/named-value/certificate/tag/group/subscription/user resource read/update editors with canonical activation and secret/key/material redaction, policy read/update editor using canonical validation/activation, and deterministic backend fault injection
C# expression member inventorypartialP2.NET value model, lexer, evaluator, and context binder for literals, grouping, arithmetic, string concatenation, comparisons, boolean short-circuit, ternary, identifiers, member access, calls, indexing, statement blocks with expression-scoped var locals, if/else, and a required return on every path, choose/mutation/retry request and response members, Headers.Get, Url.Port, request/response Body.AsString(), last-error message on retry and via stateEnv for on-error/choose, snapshot-backed context.Api/Operation/Product/Subscription/User/Deployment scalars, a MEASURED inventory of Azure’s documented context graph, which is the correction this row most needed: the allowlist was maintained from the emulator’s side and the generated ledger derived from it, so the two agreed with each other and neither could show a member Azure documents that nobody here had heard of. It reported 57 of 66 bound — 86% — while 43 documented members were absent from the list entirely. Documented() now carries Microsoft’s published surface and a gate requires every entry in it to be classified, which moved the honest figure to 54 of 104 bound (52%) with 50 planned. Binding the scalars the gateway already had state for took that to 79 of 104, and the request-time facts take it to 92 of 113 (81%) — the denominator grew because X509Certificate2 joined the documented graph when Request.Certificate and Deployment.Certificates were bound: Api.{Revision,Version,IsCurrentRevision,ServiceUrl}, Deployment.{ServiceId,GatewayId,Gateway} with the whole Gateway type, context.{Timestamp,Elapsed,RequestId,Tracing}, Subscription.{Key,PrimaryKey,SecondaryKey,CreatedDate,StartDate,EndDate} and Product.{State,ApprovalRequired,SubscriptionRequired,SubscriptionsLimit}. Subscription.Key is the key the CALLER presented rather than the primary, so two callers sharing a subscription do not collapse into one rate-limit bucket; Product.SubscriptionsLimit is null rather than zero when unset, because zero reads as “no subscriptions allowed”; Elapsed renders as .NET renders a TimeSpan, which is what a policy comparing against a literal was written for; and Deployment.Gateway.IsManaged distinguishes the built-in gateway from a self-hosted one. Product and Subscription have their own context types rather than sharing one with User, because Azure’s IProduct, ISubscription and IUser are different types and one struct carrying all of them would let a policy read a product field off a user and get an empty string instead of an error. Request.OriginalUrl is the URL the CALLER sent, captured before a policy can rewrite it, so a policy that logs or routes on where a request was aimed sees the original; MatchedParameters comes from the SAME matcher routing used, extended to report its bindings rather than recomputed alongside it, because two implementations of one match can disagree; and Certificate.Verify() checks VALIDITY ONLY — there is no trust store here, and answering true from a chain nobody built would be the dangerous direction. A gap this closed that the previous slice created: context.{Timestamp,Elapsed,RequestId,Tracing} were bound on the binder without being carried on policy.State, so they evaluated as zero through any real policy while the binder’s own tests passed on a hand-built context. A member can be bound and still be empty; there is now an end-to-end test that reads all four through an actual policy. LastError now reports WHERE a failure happened, which is what an on-error policy routes on rather than merely logs: Source/Element (the policy element that failed, recorded at compile time in the one place actions are built), Section (set as each section runs), Scope (stamped per policy DOCUMENT at compile time, because after composition merges service, product, API and operation policies into one plan nothing can tell which document an action came from), and ElementPath as section/element. An inner frame’s element survives an outer one: a failure inside a choose branch reports the branch’s element, not choose. Reason is populated only for expression-evaluation failures, the one cause this engine can identify; Azure’s full reason vocabulary is NOT reproduced, and an unclassifiable failure reads empty rather than being sorted into a bucket an on-error policy might switch on. Nesting is a stated simplification too: ElementPath is the section and the failing element, not the deeper path Azure reports for a nested element. The identity graph is bound too: User.{Email,FirstName,LastName,Note,RegistrationDate,Groups,Identities} resolved from the SUBSCRIPTION’S OWNER, with Group and UserIdentity as element types and Product.{Groups,Apis} as the grants that product carries. The graph is loaded into the runtime snapshot at activation, never queried per request: a policy expression must not put the store on the hot path. A subscription with no owner leaves context.User NULL rather than inventing an anonymous one, because context.User != null is a question a policy asks. Collections are bound but this expression language has no lambdas, so no LINQ operator exists — a policy written as context.User.Groups.Any(g => g.Name == "admin") fails loudly with a message saying so, rather than quietly returning false. Count and positional indexing work; an out-of-range index is an error rather than null, since a null would resurface later as a confusing member-access-on-null somewhere else. The last 2 planned members are bound and the documented inventory is now 113 of 113, with Body.AsString and User.Name carried as extensions beyond it. Both needed generic call syntax, so the parser reads a type argument: Body.As<string>() is the raw body and As<JObject>()/<JToken>()/<JArray>() is parsed JSON a policy can INDEX INTO, which is the entire reason to ask for an object rather than a string, and a type this emulator cannot produce is refused rather than downgraded to a string that would fail somewhere far from the cause. < is still a comparison: a type argument is only read as one when an argument list follows it, so a.B < c and a.B < c > d rewind and parse as comparisons. AsFormUrlEncodedContent() returns each field as a COLLECTION rather than a single value, because Azure returns IDictionary<string, IList> and a field may legitimately repeat. Lambdas now parse and Any evaluates, so context.User.Groups.Any(g => g.Name == "admin"), the example the previous revision of this row said would fail, answers. A lambda evaluates to a CALLABLE rather than to a result, so an operator invokes it through the same call machinery a policy uses anywhere else, and its parameter is bound in a COPIED scope so one element cannot leak into the next iteration or out to the enclosing expression. One parameter only: (a, b) => ... is refused rather than told apart from a parenthesised expression. Any is the ONLY operator, and Where, Select and the rest still fail loudly naming what exists; a predicate answering with something other than a boolean is an error rather than a truthiness test, because a policy whose predicate returns a string has a bug and guessing would hide it. The row stays partial, and the measure that says so is now external: against Microsoft’s own MIT-licensed policy corpus (Azure/api-management-policy-snippets, 59 documents, 322 distinct expressions) this parser reads 229, and adding lambdas moved that number by ZERO, because all 7 corpus expressions that use a lambda also need interpolated strings, new, or typed locals. That corpus ranks the real gaps in an order our own list never would: interpolated strings (47), new (11), lambda-adjacent syntax (11), typed locals (5). The denominator itself was wrong, and the correction is the most important thing in this row. Documented() was HAND-TRANSCRIBED from Microsoft’s reference, which is the same self-referential trap one level up: a hand-copied record of somebody else’s document drifts from it silently, and it had. It carried LastError.Element and ElementPath, which Microsoft does not document, and Gateway.RegionName and GraphQL.Arguments, likewise; it was missing LastError.Path and PolicyId, which Microsoft does document, and context.Backend, context.Workspace and Deployment.SustainabilityInfo entirely. So 113 of 113 was 100% of a denominator we wrote ourselves. The surface is now DERIVED by scripts/derive_expression_surface.py from two Microsoft sources vendored at pinned commits under third_party/microsoft/: the published policy-expressions reference and the MIT policy toolkit’s C# interfaces. Neither is complete alone, so each member records which sources name it and the disagreements stay visible rather than being averaged: the reference says Product.SubscriptionsLimit, the toolkit says SubscriptionLimit, and the ledger carries both. Planned rows are COMPUTED as documented-minus-implemented rather than written down, so a member Microsoft adds becomes a planned row the moment the vendored source is refreshed, with nobody needing to notice. A third status, framework, separates a member of an allowed .NET type Microsoft lists without enumerating (the X509Certificate2 behind context.Request.Certificate) from a member of the documented context graph and from an extension that is purely ours. The honest figure against Microsoft’s derived surface is 89 of 131 documented members implemented, with 42 planned, 7 framework and 6 extension, and ./scripts/derive_expression_surface.py --check runs in CI so a re-vendoring nobody regenerated fails the build. The six extensions are then gone, because Microsoft permits no extensions. The expression language is a closed allowlist: the reference enumerates the context graph and the allowed .NET types, and there is no plugin surface, so an extension was never a sanctioned category, only our label for a member that works here and fails in a tenant. Microsoft’s own corpus uses none of them, and uses Body.As<string>() seven times where we offered AsString() zero. So LastError.Element and ElementPath, Gateway.RegionName, User.Name and Body.AsString are REMOVED rather than relabelled, and GraphQL.Arguments is renamed to the documented GraphQLArguments. LastError.Source keeps the value Element used to duplicate, which is what Microsoft defines it as. The derivation also missed Microsoft’s METHOD rows, which is how Headers.GetValueOrDefault came to be filed as an undocumented helper: reading them takes the surface to 142 and adds Variables.GetValueOrDefault, Query.GetValueOrDefault, Certificate.VerifyNoRevocation and the string extension methods to the measured gap. Five of those are now BOUND, taking the figure to 96 of 142 (68%): Variables.GetValueOrDefault, Query.GetValueOrDefault, Certificate.VerifyNoRevocation, Product.SubscriptionLimit (the toolkit’s spelling, answering the same value as the reference’s SubscriptionsLimit, because a policy written against either document is a policy that works in a tenant) and LastError.PolicyId, which reports the author’s id attribute and is recorded at compile time in the one place actions are built, empty when the author set none rather than falling back to the element name. It is asserted through a REAL policy rather than off a hand-built error, because this package has already shipped members that were bound and still arrived empty through the gateway. And a defect no member-name inventory could see: Url.Query was bound and returned the raw query TEXT, where both Microsoft sources type it IReadOnlyDictionary<string, string[]>. The member was present and only its shape was wrong, so every gate passed. It is now a dictionary answering GetValueOrDefault, ContainsKey, Count and an indexer that yields the parameter’s VALUES, since a parameter may repeat and Query["x"][0] is how a policy reads one; the raw text still reads through QueryString. Request.Headers and Response.Headers carry the same divergence — both sources type them as dictionaries of string[] while this emulator indexes them to a single string — and that is recorded here rather than fixed, because it is a wider change than this row’s slice. The inventory grades member PRESENCE, not member TYPE, which is the next thing it should learn to measure. A further 17 members are bound, taking the figure to 113 of 142 (80%), most of them the JWT cluster: AsJwt() on a string, the ten Jwt members and Claims. Header and payload are read separately because Algorithm and Type live in the HEADER, and reading them off the payload would answer empty for every real token; the signature is NOT verified, because this reads a token and validate-jwt is what checks one. AsJwt() and AsBasic() answer NULL for input they cannot parse rather than failing, which is Microsoft’s documented contract and the reason AsJwt() != null is a question a policy can ask. An unreadable HEADER still yields the claims: the payload is what decides, and validate-jwt has always read the payload alone, so tightening that here would have changed which tokens the gateway accepts. Jwt.Claims is a dictionary of VALUES like the query string, aud reads as a collection whether the token spells it as a string or an array, and the three DateTime? members are null when absent rather than the unix epoch, which a policy comparing against now would read as long past. context.Trace records from an expression under the source expression, and is a NO-OP where nothing is collecting, because a policy that traces should not break when tracing is off. Api.Protocols comes from the api the request routed to. The 29 still planned are the ones with no data behind them here — Foundry, AzureVnetInfo, PrivateEndpointConnection, SustainabilityInfo — plus TryParseJwt and TryParseBasic, whose out parameter this expression language has no way to spell, and LastError.Path, which needs nesting indices the compiler does not yet record. context.Backend is bound too, taking the figure to 116 of 142 (82%): Id and Type report the backend a policy actually routed to, resolved from the service’s catalogue when set-backend-service names one. It is NULL until a policy names one, because an api served straight from its serviceUrl has no backend resource and context.Backend == null is the question a policy asks to find out whether one was chosen. Type is read from the backend’s own document rather than assumed: this emulator does not implement backend pools, but a pool backend can still be created through ARM, and reporting it as Single would be wrong in exactly the case a policy is asking about. Backend.AzureRegion stays PLANNED beside its two bound siblings, because nothing here records a backend’s region and answering the service’s own would be a guess a policy could route on. All of it is asserted through a real request rather than a hand-built state, since the failure this guards is the gateway never supplying the catalogue, which would leave the member bound and permanently null. The header divergence this row recorded is now fixed: Request.Headers and Response.Headers index to the header’s VALUES rather than to the first of them, which is how Microsoft types them and what a repeated header needs. Set-Cookie routinely repeats, and the old shape read fine for the single-valued case while silently dropping the rest, so a policy written here carried a wrong assumption into a tenant. GetValueOrDefault still answers a STRING, because Microsoft documents it as returning one. An ABSENT header still reads null rather than failing, and that is a STATED leniency rather than a match: .NET throws on a missing dictionary key, and this evaluator answers null for every other absent lookup, so consistency here was chosen over fidelity and is written down instead of being left to be discovered. So the inventory now grades TYPE as well as presence. Both vendored sources declare a C# type per member, the derivation records it, and a gate maps that declaration onto the shape a value must have: string, bool, a number, a collection, a dictionary, or an object. 98 bound members are checked against it. A dictionary satisfies a declaration of object because it is one with more; the REVERSE never holds, and that asymmetry is the entire gate, because answering a plain object where Microsoft declares a dictionary is exactly how the header defect passed everything else. Methods are skipped rather than called, since calling one with the wrong arguments fails for reasons that have nothing to do with its type, and null is skipped because it carries no shape. The gate reaches members through one PATH PER TYPE rather than an expression per member — 29 paths against 130 expressions — so it cannot quietly check the wrong thing, and it fails if it ever checks fewer than 60. It found two more defects on its first run. Api.ServiceUrl answered the raw text where both sources declare IUrl, so ServiceUrl.Host failed here and works in a tenant; it is now a url, and because a url renders as its own text the change is additive rather than breaking. And request and response headers answered a plain object because they lacked ContainsKey and Count, which an IReadOnlyDictionary has: both are now bound as framework members. Enums are checked as STRINGS because this evaluator renders an enum as its name, which is what a policy comparing against a literal was written for. And the corpus is now a gate rather than a measurement. Microsoft’s 59 published policy documents are vendored at a pinned commit, every @(...) and @{...} is extracted from them after XML-unescaping, and each is parsed: 229 of 322 read. This measures the LANGUAGE where every other gate here measures the SURFACE, and the distinction is the point — a policy is an expression, not a member lookup, so an expression this parser cannot read fails whatever the inventory says. It is a RATCHET, not a floor: an expression that parses today must keep parsing, and a run that parses more must regenerate the baseline. A percentage floor would let one expression regress while another improves and report the same number, which is exactly the shape of failure this repository keeps finding. The baseline ranks the gaps, and they are not the ones our own list would have guessed: interpolated strings (47), new (11), lambda-adjacent syntax (9), typed locals (5). Interpolated strings alone were a bigger hole than every unbound member combined, and they are now implemented: $"..." lexes and evaluates, taking the corpus from 229 to 266 of 322 (83%) and removing that gap entirely. The design points are where the corpus disagreed with what a reading of C# would have suggested. A hole is scanned with brace depth AND string awareness rather than by finding the next }, because Microsoft’s own policies write $"{x.ToString("R")}" and $"{context.Variables["state"]}", putting quotes inside a hole inside a quoted string. ONE scanner serves the lexer, which needs the string’s end, and the parser, which needs its parts, since two scanners over the same syntax are two chances to disagree about where a hole stops. Interpolation is its own node rather than sugar for +, so a NULL hole renders as empty the way C# renders it instead of inheriting whatever addition does with a null operand. Alignment and format specifiers ({value,10:F2}) are REFUSED rather than dropped, because rendering a formatted value unformatted is silently wrong. {{ is a literal brace per C#, which does not collide with APIM’s {{NamedValue}} because named values are substituted into the document before an expression is compiled. What the corpus reported next was not what our own list would have guessed: with interpolation gone, the ranked gaps were new (12), lambda-adjacent syntax (9), and what looked like object initialisers (9). The last of those did not exist. Every one was a bare {{NamedValue}}, which the gateway substitutes into a policy document BEFORE an expression is compiled, so the emulator never sees one: the gate was parsing raw snippets and reporting a language gap that only its own measurement created. Substituting named values the way the gateway does, which is what the gate should have done from the start, takes the corpus to 275 and removes that category entirely. new is now implemented and the corpus reads 281 of 322 (87%). Construction is an ALLOWLIST keyed by type, the same way casts are: new XDocument(...) is a parse error NAMING the type rather than something that compiles and fails when a request arrives, which also keeps the corpus measuring what works rather than what merely parses. Random is seeded reproducibly when a policy passes a seed, and its upper bound is EXCLUSIVE as .NET’s is, because a traffic split written Next(1, 100) sends requests to a different bucket if that is off by one; an empty or inverted range reports rather than answering a number. Uri is a distinct type from APIM’s IUrl and carries .NET’s member names, so Uri.Query keeps its leading ? where IUrl.Query is a dictionary: two types, two shapes, and conflating them would answer the wrong one. JObject, JProperty and anonymous objects are one value that renders as JSON in SOURCE order, since that is a response body and Newtonsoft preserves the order. A boolean renders LOWERCASE there while this evaluator renders one as .NET’s True everywhere else, because {"x":True} is not JSON — a defect the tests caught rather than the corpus, which only measures parsing. Locals may now be declared with an explicit type, string raw = ... and byte[] bytes = ... and System.String s = ..., taking the corpus to 284 of 322 (88%). The shape is self-disambiguating — a C# expression statement cannot be two identifiers in a row — so unlike a cast or a new, no allowlist of type names is needed and an unknown type is still a valid declaration. The declared type is then DISCARDED, and that is a divergence in the permissive direction: string x = 5; is accepted here and rejected by Azure. It is recorded rather than hidden, because checking it means C#‘s conversion rules and approximating those would reject valid policies, which is the worse failure. What surfaced underneath is the honest remainder: assignment to an existing local, const, local functions, foreach, try and using, plus the types nobody has built — each now failing with a message naming exactly what it needs rather than as a category. A policy block is now a sequence of STATEMENTS rather than a list of declarations with one result, which is what if (x == null) { x = fallback; } needs: the branch falls through instead of having to produce a value, and the assignment is visible to the statements after it. An else is therefore OPTIONAL, where it used to be required because an if compiled to a ternary and both arms had to yield something, and else if chains without braces. Assigning to a name nobody DECLARED is an error rather than an implicit declaration, so a policy’s typo cannot quietly become a variable that reads null everywhere else; and a local declared inside a branch is removed when the branch ends, because C# scopes it there and letting it escape would make a policy work here that fails in a tenant. A block must still return on every path, checked at compile time, with an if counting only when BOTH branches return. A statement after a return is now unreachable rather than illegal, which is what C# does with it. The corpus reads 296 of 322 (92%), after the null-conditional operator and one missing cast type. ?. answers NULL for a null receiver, and short-circuits the REST of the chain rather than only its own link, which is C#‘s rule: a?.b.c is null when a is, instead of failing on .c. It is opt-in, so an unguarded access on null still fails loudly — making every access null-tolerant would hide the mistakes ?. exists to let a policy handle deliberately. Jwt joined the castable types, which it should have when Jwt was bound: a type this evaluator answers members on was unreachable through ((Jwt)x).Claims, the idiom real policies use. That cast is not what blocks the corpus expressions using it, though: they read the token out of context.Variables, and a variable holds TEXT here rather than an object, so they parse and then fail on the member. Object-valued variables are the actual gap and are recorded as such rather than papered over. Four of the remaining failures are not language gaps at all: 45 of the 59 vendored documents are not well-formed XML, because Microsoft’s snippets write unescaped " and < inside attributes, and where an expression sits in an attribute broken that way the extracted span picks up the surrounding markup. Those four cannot be parsed by anything and put the honest ceiling at 318, not 322. A variable can now hold an OBJECT, which is what ((Jwt)context.Variables["token"]).Claims actually needed: Azure types a variable object, and this stored only the rendering, so a policy that parsed a token got text back and every member read off it failed. set-variable now keeps the value beside its rendering — the text map still holds the rendering, so every consumer reading variables as text, cache keys and headers and rate-limit keys, is untouched — and overwriting with text CLEARS the object rather than leaving a stale one to resolve against. Credentials from get-authorization-context and stored objects share one namespace, because a policy reads both through context.Variables, with a stored object winning since it was written later. The corpus number does not move for this, and that is the right outcome: those expressions already parsed and were failing at evaluation, which is precisely the blind spot of a gate that measures parsing. ?? and ?[ complete C#‘s three null operators, which ?. alone had left two thirds done: the corpus writes all three in one expression, ...?[1] ?? string.Empty. ?? is RIGHT-associative and sits between the ternary and `
developer portalplannedP3portal API fixtures and Playwright journeys
workspacespartialP4Workspaces as a SCOPE, not a resource kind: the router peels a /workspaces/{id} segment and every family the emulator implements at service scope is then available inside it, parented to the workspace, with no per-family work. The store’s parent model was rebuilt for this: a scopes table that services and workspaces both register in, with 20 resource foreign keys repointed at it, plus a migration that rebuilds a pre-workspace database in one all-or-nothing script driven from sqlite_master. Isolation is exact, not prefix-based, so the same API name can exist in both scopes and neither listing sees the other. Deleting a workspace cascades through its scope to everything inside it; deleting a service takes its workspaces with it. Workspace CRUD, listing, and workspace-scoped policies included. Witnessed by Microsoft’s JavaScript SDK, which composes the workspace-scoped URLs and IDs from its own model of the hierarchy. Workspace RBAC is modelled: see the Azure RBAC row. The peeling is family-blind, which is what let workspaces ship without per-family work, and the price of that is paid by an explicit list: the eight families Azure scopes to a SERVICE only (caches, identityProviders, openidConnectProviders, authorizationProviders, authorizationServers, documentations, gateways, users) are refused under a workspace with a 404 before any store write, because the emulator would otherwise create them there happily and the caller would find out only when the same flow reached a real tenant. The list is derived from which families @azure/arm-apimanagement@10.0.0 publishes a Workspace* operation group for, so it is evidence from one SDK version rather than proof, and it is asserted exactly rather than left implicit — a family added to it by accident silently removes a working surface, so the test carries a control of families that must still be creatable in a workspace. Only the FIRST segment after the workspace is matched, because users is service-only as a DIRECTORY while WorkspaceGroupUser is real: a workspace group’s members are resolved against the service’s user directory, so /workspaces/{id}/groups/{g}/users/{u} works and /workspaces/{id}/users/{u} does not. Workspace gateways, and the families the emulator does not implement at service scope either, remain pending
Azure RBACpartialP4Microsoft.Authorization role assignments and role definitions at any ARM scope: subscription, resource group, service, or workspace. The published built-in role GUIDs are served unchanged, because tooling hard-codes them. Evaluation follows Azure’s model: a definition’s actions are matched with wildcards and reduced by notActions, an assignment inherits down the resource-ID prefix with segment boundaries respected, and the decision is deny-by-default. Enforcement is opt-in (APIM_ENFORCE_RBAC), because the default of a valid ARM token meaning full access is what every existing caller assumes; enabling it requires APIM_RBAC_OWNER, since creating a role assignment is itself an action needing a role and without a bootstrap nobody could ever grant the first one. A refusal is ARM’s own AuthorizationFailed 403 naming the action and scope. Witnessed by Microsoft’s @azure/arm-authorization SDK, a second provider’s client against the same emulator. Pending: custom role definitions, deny assignments, principalType validation against a directory, and Microsoft.Authorization actions beyond role assignments
self-hosted/workspace gatewayspartialP4The self-hosted gateway’s management plane and its runtime consequence. A gateway is a REGISTRATION of a process that runs somewhere else: service/{svc}/gateways/{id} with locationData, a key pair issued once at registration, the APIs it is permitted to serve, the hostnames it answers on, and per-gateway certificate-authority trust. The runtime half is what makes the association more than a label: a request arriving on a gateway’s hostname is served by THAT gateway, so an API not associated with it is not refused there, it is absent — while both stay reachable on the service’s own front door. Detaching an API, or deleting the gateway, takes it off on the next activation. Keys never appear on a GET; listKeys is the only way to read them, and generateToken mints {id}&{expiry}&base64(HMAC-SHA512(key, "{id}\n{expiry}")) with Azure’s documented 30-day ceiling. managed is refused as a gateway id because it names the service’s own built-in gateway. There is deliberately no workspace-scoped gateways path: Azure has none, so it 404s rather than silently resolving to the service. Witnessed by Microsoft’s JavaScript SDK across all four of its gateway operation groups, with the token signature RECOMPUTED in node from the key listKeys returned rather than compared to a string the emulator also produced, and the isolation driven over the wire against a live gateway hostname. Explicitly NOT implemented and not implied: the configuration-sync protocol the self-hosted gateway container speaks. Its payload format is proprietary and has never been captured here, so no real self-hosted gateway has ever connected to this emulator; a fabricated config endpoint would pass its own tests forever and prove nothing. Also pending: the token format is derived from Microsoft’s published sample rather than a capture of Azure’s own generateToken; workspace gateways (the separate top-level Microsoft.ApiManagement/gateways resource with its own SKU and configConnections); listDebugCredentials, invalidateDebugCredentials and listTrace, which answer 501 rather than pretending not to exist; and heartbeat, metrics, disconnected and fail-static behaviour
SOAPimplementedP5Pass-through SOAP. WSDL import through format: "wsdl" / "wsdl-link" on the API itself, which is where Azure puts it, deriving one APIM operation per WSDL operation and stamping apiType: soap. Requests route by SOAPAction or, when a caller omits it, by the first element inside the envelope Body, so SOAP 1.2 callers and WS-I-permitted empty actions both resolve. The envelope is forwarded byte for byte, so a WS-Security signed document still verifies. An operation the WSDL does not define is refused at the gateway as a SOAP fault, shaped for the caller’s version: 1.1 faultcode/faultstring, 1.2 Code/Value and Reason/Text, since a 1.1 body is undecodable to a 1.2 stack. Witnessed by the soap npm package on both ends, a real SOAP server behind the gateway and a real client in front. Pending: SOAP-to-REST transformation, WSDL exposure at ?wsdl, wsdlSelector service/endpoint filtering, and XSD-level request validation
GraphQLimplementedP5Both APIM GraphQL shapes. Pass-through: schema import as the application/vnd.ms-azure-apim.graphql.schema resource, the JSON POST, application/graphql POST and GET transports, schema validation refusing invalid operations at the gateway, and introspection answered from the stored schema. Synthetic: resolvers bound to a Type/field coordinate, each running an <http-data-source> against a REST backend, reading its field arguments through context.GraphQL.Arguments and its parent object through context.GraphQL.Parent, with nested resolvers, selection projection, and GraphQL’s partial-failure contract (a failed field is null with a path in errors while its siblings still resolve). Witnessed by the reference graphql implementation on both ends: as the pass-through backend, as the client, and as the oracle that rebuilds the emulator’s introspection with buildClientSchema and compares printSchema to the imported SDL. Pending: resolver <http-response> mapping (refused, not ignored), validate-graphql-request depth and size limits, and the Azure SQL and Cosmos DB data sources
WebSocket/SSEimplementedP1/P5SSE event-stream flushing, text/binary WebSocket upgrade tunneling, backend proxying, and cancellation-compatible pass-through
gRPCimplementedP5Pass-through gRPC over HTTP/2. Protobuf schema import as the application/vnd.ms-azure-apim.grpc.schema resource, method routing by /package.Service/Method, metadata forwarding, streamed bodies flushed per chunk so server-streaming delivers messages as they arrive, and trailers, which is where gRPC carries the call status. A method absent from the schema is refused at the gateway with UNIMPLEMENTED and never reaches the backend. Required enabling HTTP/2 on both legs: ALPN plus h2c on the listener, and an explicit HTTP/2 transport outbound, since Go negotiates h2 only over TLS. Witnessed by grpc-js on both ends, a real gRPC server behind the gateway and a real client in front. Pending: client and bidirectional streaming, deadline propagation, gRPC-Web transcoding, and tier constraints
networking/private/custom domainspartialP1/P6Custom-hostname routing with exact host precedence, backend TLS chain-validation and client-certificate behaviour, retry execution, and local gateway reachability. Private networking is the management contract and the approval workflow: privateEndpointConnections arrive Pending and are approved or rejected by the service owner, with the decision canonicalised so “approved” and “Approved” are one state; provisioningState stays Succeeded through a rejection, because a refused connection is still a successfully written resource and conflating the two makes a rejection look like a failed write. privateLinkResources advertises the one sub-resource an APIM service actually exposes, Gateway, and 404s anything else rather than implying more. networkstatus is served in both shapes the SDK models — a list by location and a single per-location status — alongside outboundNetworkDependenciesEndpoints. Witnessed by Microsoft’s JavaScript SDK across all three operation groups. The witness is deliberately two-party, and that came from reading the SDK rather than the docs: PrivateEndpointConnectionRequest carries only the connection STATE and has no field for the endpoint, because in Azure the endpoint is created by the consumer through the Network resource provider and APIM surfaces what arrived. So the request is seeded out-of-band and the SDK drives only the decision, which is the real division of labour. What is NOT claimed, and could not be: a private endpoint lives in a consumer’s virtual network and this emulator never reaches one, so no private connectivity is emulated and none is implied. The connectivity statuses all report success because there is no virtual network here to be misconfigured; a synthesised failure would be a fault the emulator invented and an operator would be debugging fiction. The outbound dependency list is the emulator’s own and short on purpose, not a capture of a real deployment’s. Custom domains carry a secret and a fact in one object, and are handled as such: encodedCertificate and certificatePassword are write-only and dropped before storage, while the subject, thumbprint and expiry inside the PFX are parsed out and reported. A certificate is classified Custom, KeyVault or Managed by where it comes from, since an operator rotating one needs to know which. An EXPIRED certificate parses perfectly and cannot serve TLS, so it is reported Failed with its expiry still visible: reporting Completed because the bytes were readable would call a domain healthy on the day it stops working. A certificate that will not parse fails ITS hostname and not the service, so one broken domain does not take a working service down. negotiateClientCertificate has a runtime consequence: a request arriving on that hostname without a client certificate is refused 403 before any policy runs, which is the nearest truthful equivalent of Azure refusing it in the TLS handshake, and it is per hostname rather than a service-wide switch. Witnessed by Microsoft’s JavaScript SDK, whose own model carries both the write-only fields and the read-only certificate facts, plus a wire-level check that the mTLS hostname refuses an unauthenticated request. Pending: defaultSslBinding is stored and reported but NOT honoured — the emulator serves one listener certificate and does not select per hostname by SNI — Key Vault certificate retrieval for a hostname (the reference is recorded, the secret is not fetched), virtual-network type and subnet configuration, and Azure differential fixtures
tiers/SKUs/regions/deploymentpartialP6A SKU catalogue in one place, because an APIM tier is not a price but which capabilities exist at all. Five classic tiers with their capacity bounds and capability sets, served through the three operation groups the SDK distinguishes: what THIS service can become (skus, which omits moves that always fail — a dedicated service is not offered Consumption and vice versa), what Azure sells (Microsoft.ApiManagement/skus, with each tier’s capabilities), and where the service is deployed (regions, master and additional). Validation is always on: an absent or unknown tier is refused, capacity is bounded per tier with a distinct refusal for a tier that cannot scale at all (Consumption runs zero units, Developer exactly one), and additionalLocations is Premium-only with each region’s own SKU validated and its gatewayRegionalUrl projected. Capability GATING is opt-in behind APIM_ENFORCE_TIERS, following the APIM_ENFORCE_RBAC precedent: with it on, workspaces are refused below Premium and self-hosted gateways below Developer/Premium, and the refusal names the tiers that do have the capability. With it off — the default — the emulator is MORE PERMISSIVE than a tenant, which is a real divergence and not a neutral default: a Developer service creates workspaces here and cannot in Azure. It is off because every existing caller, test and witness builds a Developer service and then does exactly that. Witnessed by Microsoft’s JavaScript SDK across all three SKU groups plus a capacity refusal caught as its own error and a Premium multi-region service. The catalogue is derived from Microsoft’s published feature-by-tier table, not captured from a subscription, so the capacity ceilings are documented defaults a real quota may sit below. Pending: the v2 tiers (BasicV2, StandardV2, PremiumV2), whose capability matrix differs from the classic ones and has not been verified here; availability zones; platform version; scale-out as an operation rather than a PUT; and Azure differential fixtures
diagnostics/analytics/monitoringpartialP1/P7lossless logger and service/API diagnostic ARM state, logger reference protection, fixed sampling and all-error override, correlation/status/duration/client-IP SQLite events, portal diagnostic-event navigation, Go SDK and real gateway integration, request/response header and body capture with byte limits and secret masking; external adapters, analytics, metrics, and Azure differential fixtures pending
authorization providers/integrationsimplementedP7Credential manager: APIM as the OAuth2 CLIENT for outbound calls, distinct from authorizationServers (the portal console’s server) and from validate-jwt (authenticating callers). authorizationProviders, the credentials under them, and per-credential accessPolicies, with cascade delete so withdrawing a provider revokes what it issued. Both grants: clientCredentials usable on creation, authorizationCode created unusable and connected only through getLoginLinks + confirmConsentCode, since the emulator never auto-consents. Tokens are cached, refreshed ahead of expiry, and reached from policy through get-authorization-context and ((Authorization)context.Variables[...]).AccessToken. The client secret and the refresh token never leave the emulator: neither is echoed by the management plane, and a policy sees the access token only. Witnessed by oidc-provider, a real OAuth 2.0 / OpenID Connect authorization server, which performs genuine code exchange, refresh and client-credentials grants and confirms by introspection that the token the backend received is one it issued. Pending: managed-identity and JWT identity-type, per-caller access-policy enforcement at request time, and the Azure-hosted providers (GitHub, Google, Dropbox) which need registered apps and a human, so they belong in an opt-in differential suite rather than CI
AI gateway and MCPpartialP8LLM token governance and MCP server exposure. llm-token-limit and llm-emit-token-metric, each also under its provider-specific azure-openai-* name, since Azure ships both and a configuration written against either must work. A per-counter-key sliding minute window, refusal with 429 and Retry-After, and the consumed and remaining counts surfaced to headers, to policy variables, or to both. Streamed answers are counted, and that needed a design decision rather than a policy action: an SSE body is written to the caller as it arrives, so an outbound policy cannot see it and buffering it to look would destroy the streaming; the token count is teed off the write path instead, and the bytes reaching the caller are asserted byte-identical to the model’s. Witnessed by OpenAI’s own client on both shapes, with the 429 caught as its own error type and a refused request confirmed never to reach the model. MCP: an API declared type: mcp is published as an MCP server at {path}/mcp over the Streamable HTTP transport, and ITS OPERATIONS ARE ITS TOOLS — name, description and a JSON Schema derived from the parameters the operation already declares, so an operator describes the call once. A tool call becomes the HTTP request that operation describes, against the same backend the REST callers reach. initialize negotiates a protocol revision rather than echoing whatever was asked for; a notification is answered with 202 and no body; the server-initiated GET stream is declined rather than held open; and a backend failure is reported as a FAILED TOOL (isError) while an unknown tool or a missing required argument is a JSON-RPC error, because a model needs to tell ‘the thing I called is down’ from ‘my call was wrong’. The protocol was captured from the reference client driving a probe server, not inferred from prose. Witnessed by @modelcontextprotocol/sdk, which validates every result against the protocol’s own schemas. A pre-existing defect this found: Azure’s contract carries the API type as properties.type, which is what Microsoft’s SDK sends, while this emulator read only properties.apiType. Every earlier protocol witness set the type with a raw ARM PUT, so none could see it — an API created through the official SDK was stored, echoed back on GET looking correct, and served as though it had no type at all. All four families (graphql, soap, grpc, mcp) now read both spellings and WSDL import stamps the wire name. Pending, and none of it implied: semantic caching, llm-content-safety, backend pools and load balancing, model-API import and provider adapters, Application Insights delivery of the emitted metric, MCP passthrough: an API declared type: mcp with mcpMode: passthrough puts APIM in front of an MCP server somebody else runs. Every JSON-RPC message is FORWARDED rather than interpreted, deliberately: an upstream may implement resources, prompts, sampling and protocol revisions this emulator has never heard of, and a proxy that understood the messages would silently cap it at what the emulator knows. The transport headers ride both ways (Mcp-Session-Id, Mcp-Protocol-Version), an event-stream answer is streamed rather than buffered, the server-to-client GET is forwarded (where a SYNTHESISED server declines it, which is the one place the two modes are distinguishable), and the upstream’s own refusals reach the caller unaltered. Inbound and outbound policies run, which is the reason to put APIM there at all; an outbound policy may short-circuit with its own response but cannot rewrite the upstream BODY, because it may be a stream and buffering it to edit would turn a streaming tool call into one that appears to hang. Witnessed by the reference @modelcontextprotocol/sdk on BOTH ends: a real MCP server behind the gateway and a real client in front, completing a session neither of them can tell was proxied. mcpMode is this emulator’s own property, explicit rather than inferred from an API having no operations, since that is also what a misconfigured exposure looks like. Pending: MCP resources and prompts, per-tool policy scoping, and the SSE transport older MCP clients use
Entra ARM authenticationsdk-verifiedP0Go SDK + azidentity + in-process entra-emulator
full dated stable parity auditplannedP9published evidence snapshot

The project may say a capability is supported when it is implemented, but it may say it matches Azure only at verified. A release may claim full parity only for the dated stable snapshot in which every inventory entry is classified and verified or carries a narrowly documented infrastructure substitution.