An educational Azure-first .NET project showing how a small agent system can stay interoperable by combining Azure AI Foundry model access, A2A discovery and delegation, deterministic remote-agent validation, and single-image deployment to Azure Container Apps.
This version uses an Azure AI architecture guidance scenario with one remote specialist agent, one gateway agent, Foundry-backed answer generation and synthesis, A2A-compatible card and message routes, and cloud-first deployment through Azure Container Registry and Container Apps.
This repository demonstrates a practical agent-to-agent interoperability flow:
- Load runtime configuration for mode, Foundry endpoint, API key, model, specialist base URL, allowed agent hosts, and HTTPS enforcement.
- In Specialist mode, serve a remote agent card and accept delegated A2A message requests on fixed endpoints.
- Answer specialist questions by calling a Foundry-backed model with a fixed architecture knowledge pack.
- In Gateway mode, fetch the remote agent card, validate its host, scheme, and advertised skill against deterministic policy, then delegate the user question over A2A.
- Run a second Foundry call on the gateway to convert the specialist's prose answer into structured JSON with summary, findings, actions, and confidence.
- Return the structured gateway response together with the raw specialist answer and delegated conversation context.
- Optionally protect specialist A2A routes with API key authentication using constant-time comparison.
- Build one container image, then deploy it twice to Azure Container Apps with different environment-driven modes.
The result is a real cloud-hosted A2A system without local model infrastructure, local Docker builds, or hardcoded credentials in source.
- Azure AI Foundry integration through the OpenAI-compatible inference endpoint
- Automatic resolution from Foundry project URLs to Azure OpenAI chat endpoints
- A2A-compatible ASP.NET Core endpoints for remote agent-card discovery and delegated messaging
- Deterministic validation of remote agent host, URL scheme, and required skill ID before delegation
- Gateway synthesis that converts remote specialist prose into a bounded structured JSON response
- Conditional specialist API key enforcement with constant-time comparison
- Single-image deployment promoted to both gateway and specialist roles through environment configuration
- Cloud-first build and deploy flow using Azure Container Registry build tasks and Azure Container Apps
- Startup validation that refuses to run with missing or invalid runtime configuration
- Conversation threading from gateway request through delegated A2A context
- .NET 10 SDK or later for source builds and tests
- Azure subscription
- Azure CLI logged into the target subscription
- An Azure AI Foundry project endpoint such as
https://<resource>.services.ai.azure.com/api/projects/<project> - A model deployment in that Foundry project such as
gpt-4.1-mini - For local HTTPS specialist runs, an ASP.NET Core development certificate if you want to test the gateway flow locally
The checked-in configuration defaults to Gateway mode and expects live Foundry credentials. There is no mock-model path in this project.
From the project root, verify the solution first:
dotnet test AzureFoundryA2AInteroperability.slnxFor a local end-to-end run, start the specialist first in one terminal:
$env:ASPNETCORE_URLS="https://localhost:7001"
$env:A2AINT_Runtime__Mode="Specialist"
$env:A2AINT_Foundry__BaseUrl="https://<resource>.services.ai.azure.com/api/projects/<project>"
$env:A2AINT_Foundry__ApiKey="<foundry-api-key>"
$env:A2AINT_Foundry__ModelId="gpt-4.1-mini"
$env:A2AINT_SpecialistAgent__PublicBaseUrl="https://localhost:7001"
dotnet run --project AzureFoundryA2AInteroperabilityThen start the gateway in a second terminal:
$env:ASPNETCORE_URLS="http://localhost:7002"
$env:A2AINT_Runtime__Mode="Gateway"
$env:A2AINT_Runtime__SpecialistBaseUrl="https://localhost:7001/a2a/specialist"
$env:A2AINT_Runtime__AllowedAgentHosts__0="localhost"
$env:A2AINT_Foundry__BaseUrl="https://<resource>.services.ai.azure.com/api/projects/<project>"
$env:A2AINT_Foundry__ApiKey="<foundry-api-key>"
$env:A2AINT_Foundry__ModelId="gpt-4.1-mini"
dotnet run --project AzureFoundryA2AInteroperabilityQuery the gateway:
POST http://localhost:7002/api/query
Content-Type: application/json
{
"question": "How should I host a Foundry-backed A2A system on Azure without local dependencies?",
"conversationId": "demo-session-1"
}The gateway returns a structured answer containing the discovered specialist identity, a summary, delegated findings, next actions, confidence, and the raw specialist response.
This repo includes:
infra/main.bicepfor shared Azure infrastructurescripts/publish-image.ps1to queue a cloud build in Azure Container Registryscripts/deploy.ps1to provision both Container Apps and wire the gateway to the specialist
Recommended deployment order:
- Provision shared infrastructure:
az deployment group create `
--resource-group <rg> `
--template-file .\infra\main.bicep `
--parameters location=northeurope acrName=<acrName> containerAppsEnvironmentName=<envName>- Build the image in Azure:
.\scripts\publish-image.ps1 `
-ResourceGroupName <rg> `
-AcrName <acrName> `
-ImageTag v1- Deploy the two Container Apps:
.\scripts\deploy.ps1 `
-ResourceGroupName <rg> `
-ContainerAppsEnvironmentName <envName> `
-AcrName <acrName> `
-ImageTag v1 `
-FoundryBaseUrl https://<resource>.services.ai.azure.com/api/projects/<project> `
-FoundryApiKey <foundry-api-key> `
-FoundryModelId gpt-4.1-miniIf -SpecialistApiKey is omitted, the deploy script generates a random GUID key and injects it into both container apps automatically. Retrieve the deployed value later if needed:
az containerapp show `
--resource-group <rg> `
--name a2a-specialist `
--query "properties.template.containers[0].env[?name=='A2AINT_Runtime__SpecialistApiKey'].value" `
-o tsvDefault settings are in AzureFoundryA2AInteroperability/appsettings.json.
Example:
{
"Runtime": {
"Mode": "Gateway",
"RequestTimeoutSeconds": 45,
"SpecialistBaseUrl": "https://specialist.contoso.com/a2a/specialist",
"SpecialistApiKey": "replace-me",
"A2AApiKeyHeaderName": "x-a2a-api-key",
"RequireHttpsSpecialist": true,
"AllowedAgentHosts": [ "specialist.contoso.com" ]
},
"Foundry": {
"BaseUrl": "https://YOUR-RESOURCE.services.ai.azure.com/api/projects/YOUR-PROJECT",
"ApiKey": "replace-me",
"ModelId": "gpt-4.1-mini"
},
"SpecialistAgent": {
"Name": "Foundry Architecture Specialist",
"Description": "Azure-hosted AI engineering specialist for A2A interoperability, secure delegation, and production deployment patterns.",
"Version": "1.0.0",
"SkillId": "azure_ai_architecture_review",
"PublicBaseUrl": "https://specialist.contoso.com"
}
}Environment variable overrides use prefix A2AINT_:
A2AINT_Runtime__ModeA2AINT_Runtime__RequestTimeoutSecondsA2AINT_Runtime__SpecialistBaseUrlA2AINT_Runtime__SpecialistApiKeyA2AINT_Runtime__A2AApiKeyHeaderNameA2AINT_Runtime__RequireHttpsSpecialistA2AINT_Runtime__AllowedAgentHosts__0A2AINT_Foundry__BaseUrlA2AINT_Foundry__ApiKeyA2AINT_Foundry__ModelIdA2AINT_SpecialistAgent__NameA2AINT_SpecialistAgent__DescriptionA2AINT_SpecialistAgent__VersionA2AINT_SpecialistAgent__SkillIdA2AINT_SpecialistAgent__PublicBaseUrl
Program.csloads configuration throughAppConfig.Load, validates it at startup, registers the Foundry chat client, and branches service registration and route mapping based on Gateway or Specialist mode.AppConfigenforces valid runtime mode, request timeout bounds, Foundry HTTPS endpoint requirements, required model credentials, and mode-specific specialist URL rules.FoundryConfig.GetChatEndpoint()resolves either a Foundry project URL or Azure OpenAI-style URL to the/openai/v1/chat endpoint used by the OpenAI SDK.FoundryChatClientFactorybuilds anIChatClientover the OpenAI client so the same chat abstraction is reused by both specialist answering and gateway synthesis.- In Specialist mode,
/a2a/*requests optionally pass through API key middleware, thenGET /a2a/specialist/v1/cardreturns the configuredAgentCard. POST /a2a/specialist/v1/message:streamextracts non-empty text parts from the inboundA2AMessageRequest, forwards them toSpecialistAnswerService, and returns an A2A-style agent message with a stable context ID.SpecialistInstructionFactorysupplies a fixed eight-point Azure architecture knowledge pack so the remote specialist stays inside a narrow interoperability and deployment scope.- In Gateway mode,
POST /api/queryforwards the caller request toGatewayQueryService. GatewayQueryServiceusesA2ARemoteClientto fetch the remote card and send the delegated question, then passes both the original question and remote answer intoGatewaySynthesisService.AgentCardValidationPolicydeterministically checks card name, description, absolute URL, HTTPS requirements, allowlisted host membership, and required skill ID presence before any delegation is trusted.A2ARemoteClientnormalizes the configured specialist base URL, attaches the configured API key header when present, deserializes the agent card and delegated message, and rejects empty remote text responses.GatewaySynthesisServiceprompts the Foundry model to return only JSON matching theGatewaySynthesisschema, extracts the outer JSON object, deserializes it, and normalizes findings, actions, and confidence values before returning the final gateway answer.- Both deployment modes expose
GET /healthz, andGET /returns a compact JSON description of the running role and available endpoints.
.
+-- AzureFoundryA2AInteroperability.slnx
+-- AzureFoundryA2AInteroperability/
| +-- AzureFoundryA2AInteroperability.csproj
| +-- Program.cs
| +-- appsettings.json
| +-- App/
| | +-- AppConfig.cs
| +-- Domain/
| | +-- A2AModels.cs
| +-- Properties/
| | +-- launchSettings.json
| +-- Services/
| +-- A2ARemoteClient.cs
| +-- AgentCardValidationPolicy.cs
| +-- FoundryChatClientFactory.cs
| +-- GatewayQueryService.cs
| +-- GatewaySynthesisService.cs
| +-- SpecialistAnswerService.cs
| +-- SpecialistInstructionFactory.cs
+-- AzureFoundryA2AInteroperability.Tests/
| +-- AzureFoundryA2AInteroperability.Tests.csproj
| +-- AgentCardValidationPolicyTests.cs
| +-- AppConfigTests.cs
+-- infra/
| +-- main.bicep
+-- scripts/
| +-- deploy.ps1
| +-- publish-image.ps1
+-- Dockerfile
+-- .dockerignore
+-- LICENSE
+-- README.md
- The gateway never trusts a remote specialist card without deterministic validation first
- Remote card URLs can be forced to HTTPS and restricted to an explicit host allowlist
- Delegation proceeds only if the remote specialist advertises the expected skill ID
- Specialist A2A routes can be protected with a shared API key, and comparison uses
CryptographicOperations.FixedTimeEquals - The gateway keeps control of the caller-facing API and synthesizes the remote answer into a bounded JSON contract
- Empty remote messages and malformed synthesis payloads fail closed with exceptions instead of being silently accepted
- One container image can be promoted across both runtime roles without changing source code
Run the test project from the repo root:
dotnet test AzureFoundryA2AInteroperability.slnxThe current test suite covers:
- gateway configuration validation for valid HTTPS specialist settings
- rejection of non-HTTPS specialist URLs when HTTPS is required
- acceptance of agent cards from an allowlisted host
- rejection of agent cards from an unexpected host
See the LICENSE file for details.
Contributions are welcome for improvements within current project scope.
Suggested areas:
- Add broader gateway and specialist endpoint tests beyond config and card validation
- Prefer managed identity or secret references in deployment wiring where the hosting model allows it
- Expand A2A payload handling to support richer metadata, tool results, or non-text parts
- Add observability hooks such as structured request correlation and delegated failure classification
- Tighten JSON synthesis parsing with stronger schema validation or explicit response contracts