.NETC#AI AgentsDevOpsASP.NET CoreOpenAI Responses

Exposez un agent C# via l’API OpenAI Responses, isolez les sessions, testez son contrat HTTP et déployez-le avec des garde-fous DevOps fiables en production.

Yva Hajatiana
10 septembre 2026
10 min de lecture
Partager :X / TwitterLinkedIn
Composition éditoriale abstraite sur un agent .NET auto-hébergé et son API sécurisée

Agent .NET auto-hébergé : sécuriser l’API Responses

Un agent déployé dans Microsoft Foundry est pratique lorsque la plateforme gère son runtime. Mais certaines équipes doivent conserver le routage HTTP, l’authentification, les règles réseau, le stockage et la montée en charge dans leur propre plateforme ASP.NET Core. Dans ce cas, « exposer une API d’agent » ne doit pas devenir un raccourci qui rend l’historique d’un autre utilisateur accessible avec un identifiant deviné.

Les packages d’hébergement de Microsoft Agent Framework permettent désormais d’exposer un agent C# par l’API OpenAI Responses depuis une application ASP.NET Core. Le protocole apporte conversation, streaming et clients compatibles ; l’application garde la responsabilité de son identité, de son autorisation, du cycle de vie des sessions et de sa livraison.

Ce tutoriel construit un endpoint /support/v1/responses protégé, le teste avec curl, puis ajoute une CI qui vérifie le contrat sans appeler un modèle. La logique métier reste minimale ; le sujet est la frontière d’hébergement. Il complète le guide de déploiement d’un agent Foundry : ici, l’équipe opère elle-même l’application.

API en préversion. Au 10 septembre 2026, les packages .NET d’hébergement Agent Framework sont en préversion. Épinglez une version validée, verrouillez la restauration NuGet et testez toute mise à jour dans un environnement isolé avant de la promouvoir.

Choisir l’hébergement adapté

Le protocole et le lieu d’exécution sont deux décisions différentes. L’API Responses peut être exposée par un agent Foundry ou par une application qui vous appartient.

Besoin principalChoix initialResponsable opérationnel
Runtime géré, cycle de vie d’agent fourni par FoundryAgent Foundry hébergéMicrosoft Foundry et l’équipe applicative
Réutiliser une plateforme ASP.NET Core, des règles réseau et une identité existantesAgent auto-hébergéÉquipe plateforme et équipe applicative
Reprise longue, temporisation ou approbation durableExtension DurableÉquipe applicative et infrastructure Durable Task
Échanges entre agents indépendantsA2APropriétaire de chaque agent

L’API Chat Completions est un contrat requête/réponse stateless. Préférez Responses lorsqu’un client doit continuer une conversation, consommer un flux SSE ou utiliser les identifiants previous_response_id et conversation. Cette souplesse crée une frontière de sécurité supplémentaire : ces identifiants viennent du client et ne prouvent jamais que le client a accès à l’état correspondant.

client authentifié
       |
       | JWT + POST /support/v1/responses
       v
ASP.NET Core : authentification, limite de débit, autorisation
       |
       v
agent C# + session isolée par identité
       |
       +--> Azure OpenAI avec identité de workload

Prérequis et secrets

Prévoyez le SDK .NET 10, un déploiement Azure OpenAI et une identité capable de l’appeler. Pour l’exemple local, DefaultAzureCredential utilise votre connexion Azure CLI ; en production, configurez une identité managée ou une fédération de workload. Ne donnez pas de clé Azure OpenAI au navigateur et ne la placez ni dans appsettings.json committé ni dans les logs CI.

Créez une API et ajoutez les packages. Les versions exactes sont délibérément absentes des commandes : choisissez-les une fois dans votre solution, puis conservez-les dans packages.lock.json.

dotnet new webapi --framework net10.0 --output src/SupportAgentApi
Set-Location src/SupportAgentApi

dotnet add package Azure.AI.OpenAI
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package Microsoft.Agents.AI.Hosting.OpenAI --prerelease
dotnet add package Microsoft.Agents.AI.Hosting.AspNetCore --prerelease
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet restore --use-lock-file

Définissez uniquement la configuration de développement dans la session PowerShell courante :

$env:AZURE_OPENAI_ENDPOINT = "https://<ressource>.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT_NAME = "<deploiement>"
$env:JWT_AUTHORITY = "https://<votre-issuer>/"
$env:JWT_AUDIENCE = "support-agent-api"

Le jeton JWT de l’exemple représente l’appelant de l’API, pas l’identité qui appelle le modèle. Ces deux identités doivent être distinctes : un utilisateur autorisé à converser n’obtient pas pour autant les droits Azure du service.

1. Exposer Responses derrière une politique ASP.NET Core

Remplacez Program.cs par le code suivant. Il enregistre un IChatClient Azure OpenAI, un agent nommé, l’adaptateur Responses et une politique agent-api. RequireAuthorization protège l’endpoint avant toute création ou reprise de session.

using System.Threading.RateLimiting;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.AI;
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT est requis.");
var deployment = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
    ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME est requis.");
var authority = builder.Configuration["JWT_AUTHORITY"]
    ?? throw new InvalidOperationException("JWT_AUTHORITY est requis.");
var audience = builder.Configuration["JWT_AUDIENCE"]
    ?? throw new InvalidOperationException("JWT_AUDIENCE est requis.");

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = authority;
        options.Audience = audience;
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("agent-api", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context => context.User.Claims
            .Where(claim => claim.Type is "scope" or "scp"
                || claim.Type == "http://schemas.microsoft.com/identity/claims/scope")
            .SelectMany(claim => claim.Value.Split(
                ' ', StringSplitOptions.RemoveEmptyEntries))
            .Contains("agent.invoke", StringComparer.Ordinal));
    });

builder.Services.AddHttpContextAccessor();
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.AddFixedWindowLimiter("agent", limiterOptions =>
    {
        limiterOptions.PermitLimit = 30;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 0;
    });
});

IChatClient chatClient = new AzureOpenAIClient(
        new Uri(endpoint),
        new DefaultAzureCredential())
    .GetChatClient(deployment)
    .AsIChatClient();
builder.Services.AddSingleton(chatClient);

builder.AddOpenAIResponses();
builder.AddOpenAIConversations();

var hostedAgent = builder.AddAIAgent(
    "support",
    instructions: """
        Tu aides l’équipe support. Réponds en français et n’invente jamais
        le statut d’un service ni une procédure d’exploitation.
        """);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }))
    .AllowAnonymous();
app.MapOpenAIResponses(hostedAgent, responsesPath: "/support/v1/responses")
    .RequireRateLimiting("agent")
    .RequireAuthorization("agent-api");
app.MapOpenAIConversations()
    .RequireAuthorization("agent-api");

app.Run();

La limite de débit est placée avant toute invocation du modèle : une autorisation valide ne signifie pas une capacité illimitée. Adaptez la fenêtre et le quota à votre budget, à la charge attendue et aux règles par tenant.

Le middleware ne décide pas ce qu’un appelant peut demander au modèle. Pour une API réellement multi-tenant, ajoutez une validation d’entrée, une liste blanche des options de modèle et des règles métier spécifiques à chaque outil. Une instruction dans le prompt n’est jamais un mécanisme d’autorisation.

2. Rendre la continuation de session sûre

previous_response_id et conversation sont pratiques pour continuer un échange. Ils restent des entrées non fiables. Un contrôle qui vérifie seulement que l’identifiant commence par conv_ est insuffisant : un utilisateur pourrait présenter l’identifiant d’un autre utilisateur.

Les helpers d’hébergement recommandent cette séquence :

  1. authentifier l’appelant ;
  2. autoriser son accès à la session demandée ;
  3. partitionner le stockage par tenant, utilisateur ou espace de travail ;
  4. enregistrer la session seulement après la fin du run ou du streaming.

MapOpenAIResponses possède le routage et le stockage de son endpoint. Si vous avez besoin d’un chemin, d’un format de stockage ou d’une règle de reprise qui vous est propre, construisez une route applicative avec les helpers OpenAIResponses et un AgentSessionStore. C’est alors à votre code d’authentifier et de lier chaque continuation à son propriétaire avant toute lecture.

Pour une application ASP.NET Core avec claims, ajoutez l’intégration d’isolation et gardez-la active pour ce store applicatif :

builder.Services.AddHttpContextAccessor();
builder.Services.UseClaimsBasedAgentIsolation();

// Application implementation: session serialization, encryption, and TTL.
builder.Services.AddSingleton<AgentSessionStore, RedisAgentSessionStore>();

InMemoryAgentSessionStore reste acceptable pour un poste de développement : il perd les sessions au redémarrage et ne fonctionne pas entre réplicas. En production, fournissez une implémentation durable, par exemple sur Redis, SQL Server ou Azure Cosmos DB, avec chiffrement et contrôle d’accès propres au tenant. Ne parsez jamais l’identifiant opaque et ne lui imposez ni longueur ni jeu de caractères maison.

Une session peut contenir bien plus que des messages : état d’un provider, mémoire, demandes d’approbation en attente ou références d’historique. Traitez-la comme une donnée potentiellement sensible : rétention limitée, chiffrement au repos, suppression sur demande et aucune copie dans les traces de diagnostic.

3. Tester le contrat sans dépendre d’un LLM

Démarrez d’abord le service et vérifiez le health check, qui ne dépend ni de JWT ni d’Azure OpenAI :

dotnet run
curl.exe -i http://localhost:5000/healthz

Vérifiez ensuite que l’endpoint d’agent refuse un appel anonyme. Ce test déterministe doit toujours retourner 401 ou 403, et ne doit pas faire d’appel au modèle :

curl.exe -i -X POST http://localhost:5000/support/v1/responses `
  -H "Content-Type: application/json" `
  -d "{\"input\":\"Bonjour\"}"

Avec un jeton de test émis par votre fournisseur d’identité, appelez le contrat Responses :

$token = "<jeton-contenant-le-scope-agent.invoke>"
curl.exe -sS -X POST http://localhost:5000/support/v1/responses `
  -H "Authorization: Bearer $token" `
  -H "Content-Type: application/json" `
  -d "{\"input\":\"Réponds par une confirmation courte.\"}"

Le dernier test est une intégration : il nécessite une identité Azure et peut consommer des tokens. Ne l’exécutez pas à chaque test unitaire. Exécutez-le dans un environnement de test contrôlé, avec un budget et un modèle peu coûteux. Gardez les tests unitaires pour les fonctions de validation, les outils et les politiques applicatives.

4. Ajouter une CI qui bloque les régressions de frontière

Cette pipeline sépare les contrôles rapides du déploiement. Elle compile, lance les tests et vérifie que le health check est disponible. Le test d’authentification peut être couvert par des tests d’intégration avec un schéma JWT de test, sans secret Azure.

name: Verify self-hosted agent

on:
  pull_request:
    paths:
      - "src/SupportAgentApi/**"
      - ".github/workflows/verify-agent.yml"
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  verify:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: src/SupportAgentApi

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Restore locked dependencies
        run: dotnet restore --locked-mode

      - name: Build
        run: dotnet build --no-restore --configuration Release

      - name: Verify health endpoint
        env:
          AZURE_OPENAI_ENDPOINT: "https://example.openai.azure.com/"
          AZURE_OPENAI_DEPLOYMENT_NAME: "ci-placeholder"
          JWT_AUTHORITY: "https://login.example.com/"
          JWT_AUDIENCE: "support-agent-api"
          ASPNETCORE_URLS: "http://127.0.0.1:5078"
        run: |
          dotnet run --no-build --configuration Release > "$RUNNER_TEMP/support-agent.log" 2>&1 &
          app_pid=$!
          trap 'kill "$app_pid" 2>/dev/null || true' EXIT

          for attempt in {1..20}; do
            if curl --fail --silent http://127.0.0.1:5078/healthz > /dev/null; then
              exit 0
            fi

            if ! kill -0 "$app_pid" 2>/dev/null; then
              cat "$RUNNER_TEMP/support-agent.log"
              exit 1
            fi

            sleep 1
          done

          cat "$RUNNER_TEMP/support-agent.log"
          exit 1

      - name: Test deterministic behavior
        run: dotnet test --no-build --configuration Release

Ajoutez le déploiement après ce job, dans un environnement GitHub protégé. Donnez à son identité OIDC les permissions minimales pour le runtime choisi, sans réutiliser l’identité Azure de l’agent. Un test de fumée authentifié après déploiement doit vérifier uniquement les invariants stables : réponse au protocole, version attendue et refus des requêtes non autorisées. Les évaluations de qualité LLM ont leur propre corpus, seuils et historique, comme dans Évaluer une IA .NET dans une pipeline CI.

Points de vigilance

  • Sessions inter-utilisateurs. Une autorisation sur l’endpoint ne suffit pas si le store ne partitionne pas les continuations par identité. Conservez l’isolation de claims en production et testez ce scénario avec deux sujets JWT distincts.
  • Stockage mémoire en production. Il masque les défauts lors d’un test local, puis perd l’historique à un redémarrage ou à la mise à l’échelle. Utilisez un store durable avec TTL et chiffrement.
  • Options contrôlées par le client. Ne laissez pas une requête imposer librement modèle, température, outils ou budget. Définissez une allowlist côté serveur.
  • Limites de débit et coût. Appliquez rate limiting, quotas par tenant et un timeout à l’appel de modèle. Observez tokens, durée et erreurs sans exporter les prompts en clair.
  • Streaming interrompu. Sauvegardez la session après la fin réelle du stream. Un client déconnecté ne doit pas laisser un état présenté comme entièrement produit.
  • Packages préversion. La compatibilité transitive entre Agent Framework et Microsoft.Extensions.AI évolue vite. Épinglez des versions cohérentes et mettez à jour dans une branche dédiée.

Sources

Mots-clés :.NETC#AI AgentsDevOpsASP.NET CoreOpenAI Responses
Y

Yva Hajatiana

Articles techniques sur l'ingénierie logicielle, .NET, le cloud et l'intelligence artificielle appliquée aux applications.