diff --git a/MyIA.AI.Notebooks/GenAI/Aspire/06-Aspire-GardeFous-Roslyn.ipynb b/MyIA.AI.Notebooks/GenAI/Aspire/06-Aspire-GardeFous-Roslyn.ipynb
index 569ab7b7b8..07dff5a7ef 100644
--- a/MyIA.AI.Notebooks/GenAI/Aspire/06-Aspire-GardeFous-Roslyn.ipynb
+++ b/MyIA.AI.Notebooks/GenAI/Aspire/06-Aspire-GardeFous-Roslyn.ipynb
@@ -62,10 +62,10 @@
"id": "ag-02",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:32:59.287617Z",
- "iopub.status.busy": "2026-08-28T21:32:59.279566Z",
- "iopub.status.idle": "2026-08-28T21:33:02.184370Z",
- "shell.execute_reply": "2026-08-28T21:33:02.174704Z"
+ "iopub.execute_input": "2026-09-03T10:36:51.988268Z",
+ "iopub.status.busy": "2026-09-03T10:36:51.973156Z",
+ "iopub.status.idle": "2026-09-03T10:36:55.269005Z",
+ "shell.execute_reply": "2026-09-03T10:36:55.261289Z"
},
"papermill": {
"duration": 2.916814,
@@ -78,16 +78,126 @@
},
"outputs": [
{
- "output_type": "display_data",
"data": {
- "text/html": ""
+ "text/html": [
+ "\r\n",
+ "
\r\n",
+ "
\r\n",
+ " The below script needs to be able to find the current output cell; this is an easy method to get it.\r\n",
+ "
\r\n",
+ " \r\n",
+ "
"
+ ]
},
- "metadata": {}
+ "metadata": {},
+ "output_type": "display_data"
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "Repertoire de travail : Aspire\r\n"
+ "output_type": "stream",
+ "text": [
+ "Repertoire de travail : Aspire\r\n"
+ ]
}
],
"source": [
@@ -152,10 +262,10 @@
"id": "ag-04",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:02.214920Z",
- "iopub.status.busy": "2026-08-28T21:33:02.214920Z",
- "iopub.status.idle": "2026-08-28T21:33:02.279257Z",
- "shell.execute_reply": "2026-08-28T21:33:02.279257Z"
+ "iopub.execute_input": "2026-09-03T10:36:55.272241Z",
+ "iopub.status.busy": "2026-09-03T10:36:55.271684Z",
+ "iopub.status.idle": "2026-09-03T10:36:55.382144Z",
+ "shell.execute_reply": "2026-09-03T10:36:55.381265Z"
},
"papermill": {
"duration": 0.075947,
@@ -168,9 +278,166 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "// Terrain fautif : copie de demo du motif StreamingAgent.App (notebook 04).\n// Un worker d'agent consomme une Channel -- motif canonique de la serie --\n// mais ici le code genere \"dans le style synchrone\" BLOQUE la tache d'appel\n// au LLM avec .Result au lieu de l'attendre. C'est le defaut que l'analyseur\n// AGENTGUARD001 doit attraper au `dotnet build` (ce fichier ne leve PAS\n// d'exception : le blocage ne deadlock ici qu'un contexte reduit -- le\n// notebook explique pourquoi le pattern reste mortel en production).\n\nusing System;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\n\nvar channel = Channel.CreateUnbounded();\n_ = Producer.ProduceAsync(channel.Writer, \"traduis cette phrase en anglais\");\n\nConsole.WriteLine(AgentWorker.TranslateSync(channel.Reader));\n\npublic static class AgentWorker\n{\n // Genere par agent \"pour simplifier\" : la tache async est bloquee.\n // Deux blocages .Result -- deux diagnostics attendus au build.\n public static string TranslateSync(ChannelReader inbound)\n {\n var prompt = inbound.ReadAsync().AsTask().Result; // AGENTGUARD001\n return CallLlmAsync(prompt).Result; // AGENTGUARD001\n }\n\n private static async Task CallLlmAsync(string prompt)\n {\n await Task.Delay(50); // simule la latence de l'appel LLM\n return $\"[LLM] {prompt}\";\n }\n}\n\npublic static class Producer\n{\n public static async Task ProduceAsync(ChannelWriter writer, string prompt)\n => await writer.WriteAsync(prompt);\n}\n\n// Second terrain fautif (AGENTGUARD002) : le meme agent, une autre vitesse.\n// \"Lancer et oublier\" un traitement en ecrivant async void -- la signature\n// ressemble a une async Task, mais la methode n'est pas attendable et ses\n// exceptions ne sont observees par personne. Un diagnostic AGENTGUARD002\n// attendu au build (et aucun ici n'est un handler d'evenement : pas de\n// signature (object, EventArgs)).\npublic static class AgentFireAndForget\n{\n public static void Demarrer()\n {\n SurveillerCanalAsync(); // \"rendu la main\" -- silencieusement\n }\n\n // Genere par agent \"pour simplifier\" : async void hors handler.\n public static async void SurveillerCanalAsync()\n {\n await Task.Delay(200); // simule une boucle de surveillance\n // Si l'appel LLM leve ici, personne ne l'observe : process mort.\n }\n}\n\n// Troisieme terrain fautif (AGENTGUARD003) : le meme agent, troisieme\n// vitesse. `Task.Run(() => ...)` est appele comme enonce autonome -- la\n// signature est honnete (Task, pas void), mais le retour est jete a la\n// corbeille : pas de await, pas d'affectation, pas de `_ =`, pas de\n// return. La tache s'execute en arriere-plan, ses exceptions ne sont\n// observees par personne. Un diagnostic AGENTGUARD003 attendu au build\n// (et c'est la seule forme signalee : `await Task.Run(...)`, `var t =\n// Task.Run(...)`, `_ = Task.Run(...)` et `return Task.Run(...)` sont\n// exemptes).\npublic static class AgentTaskRunFire\n{\n public static void Demarrer()\n {\n Task.Run(() => Console.WriteLine(\"ping\")); // AGENTGUARD003\n }\n}\n\n// Quatrieme terrain fautif (AGENTGUARD004) : la requete fournit un\n// CancellationToken, et la cible sait le recevoir, mais le code genere omet\n// l'argument optionnel. L'annulation est perdue au milieu de la chaine.\npublic static class AgentCancellation\n{\n public static async Task RepondreAsync(\n string prompt,\n System.Threading.CancellationToken cancellationToken)\n {\n await CallLlmAsync(prompt); // AGENTGUARD004\n }\n\n private static Task CallLlmAsync(\n string prompt,\n System.Threading.CancellationToken cancellationToken = default)\n => Task.FromResult($\"[LLM] {prompt}\");\n}\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "// Terrain fautif : copie de demo du motif StreamingAgent.App (notebook 04).\n",
+ "// Un worker d'agent consomme une Channel -- motif canonique de la serie --\n",
+ "// mais ici le code genere \"dans le style synchrone\" BLOQUE la tache d'appel\n",
+ "// au LLM avec .Result au lieu de l'attendre. C'est le defaut que l'analyseur\n",
+ "// AGENTGUARD001 doit attraper au `dotnet build` (ce fichier ne leve PAS\n",
+ "// d'exception : le blocage ne deadlock ici qu'un contexte reduit -- le\n",
+ "// notebook explique pourquoi le pattern reste mortel en production).\n",
+ "\n",
+ "using System;\n",
+ "using System.Threading.Channels;\n",
+ "using System.Threading.Tasks;\n",
+ "\n",
+ "var channel = Channel.CreateUnbounded();\n",
+ "_ = Producer.ProduceAsync(channel.Writer, \"traduis cette phrase en anglais\");\n",
+ "\n",
+ "Console.WriteLine(AgentWorker.TranslateSync(channel.Reader));\n",
+ "\n",
+ "public static class AgentWorker\n",
+ "{\n",
+ " // Genere par agent \"pour simplifier\" : la tache async est bloquee.\n",
+ " // Deux blocages .Result -- deux diagnostics attendus au build.\n",
+ " public static string TranslateSync(ChannelReader inbound)\n",
+ " {\n",
+ " var prompt = inbound.ReadAsync().AsTask().Result; // AGENTGUARD001\n",
+ " return CallLlmAsync(prompt).Result; // AGENTGUARD001\n",
+ " }\n",
+ "\n",
+ " private static async Task CallLlmAsync(string prompt)\n",
+ " {\n",
+ " await Task.Delay(50); // simule la latence de l'appel LLM\n",
+ " return $\"[LLM] {prompt}\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public static class Producer\n",
+ "{\n",
+ " public static async Task ProduceAsync(ChannelWriter writer, string prompt)\n",
+ " => await writer.WriteAsync(prompt);\n",
+ "}\n",
+ "\n",
+ "// Second terrain fautif (AGENTGUARD002) : le meme agent, une autre vitesse.\n",
+ "// \"Lancer et oublier\" un traitement en ecrivant async void -- la signature\n",
+ "// ressemble a une async Task, mais la methode n'est pas attendable et ses\n",
+ "// exceptions ne sont observees par personne. Un diagnostic AGENTGUARD002\n",
+ "// attendu au build (et aucun ici n'est un handler d'evenement : pas de\n",
+ "// signature (object, EventArgs)).\n",
+ "public static class AgentFireAndForget\n",
+ "{\n",
+ " public static void Demarrer()\n",
+ " {\n",
+ " SurveillerCanalAsync(); // \"rendu la main\" -- silencieusement\n",
+ " }\n",
+ "\n",
+ " // Genere par agent \"pour simplifier\" : async void hors handler.\n",
+ " public static async void SurveillerCanalAsync()\n",
+ " {\n",
+ " await Task.Delay(200); // simule une boucle de surveillance\n",
+ " // Si l'appel LLM leve ici, personne ne l'observe : process mort.\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Troisieme terrain fautif (AGENTGUARD003) : le meme agent, troisieme\n",
+ "// vitesse. `Task.Run(() => ...)` est appele comme enonce autonome -- la\n",
+ "// signature est honnete (Task, pas void), mais le retour est jete a la\n",
+ "// corbeille : pas de await, pas d'affectation, pas de `_ =`, pas de\n",
+ "// return. La tache s'execute en arriere-plan, ses exceptions ne sont\n",
+ "// observees par personne. Un diagnostic AGENTGUARD003 attendu au build\n",
+ "// (et c'est la seule forme signalee : `await Task.Run(...)`, `var t =\n",
+ "// Task.Run(...)`, `_ = Task.Run(...)` et `return Task.Run(...)` sont\n",
+ "// exemptes).\n",
+ "public static class AgentTaskRunFire\n",
+ "{\n",
+ " public static void Demarrer()\n",
+ " {\n",
+ " Task.Run(() => Console.WriteLine(\"ping\")); // AGENTGUARD003\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Quatrieme terrain fautif (AGENTGUARD004) : la requete fournit un\n",
+ "// CancellationToken, et la cible sait le recevoir, mais le code genere omet\n",
+ "// l'argument optionnel. L'annulation est perdue au milieu de la chaine.\n",
+ "public static class AgentCancellation\n",
+ "{\n",
+ " public static async Task RepondreAsync(\n",
+ " string prompt,\n",
+ " System.Threading.CancellationToken cancellationToken)\n",
+ " {\n",
+ " await CallLlmAsync(prompt); // AGENTGUARD004\n",
+ " }\n",
+ "\n",
+ " private static Task CallLlmAsync(\n",
+ " string prompt,\n",
+ " System.Threading.CancellationToken cancellationToken = default)\n",
+ " => Task.FromResult($\"[LLM] {prompt}\");\n",
+ "}\n",
+ "\n",
+ "// Cinquieme terrain fautif (AGENTGUARD005) : variante syntaxique d'\n",
+ "// AGENTGUARD001, meme defaut semantique. Au lieu de `.Result`, l'agent\n",
+ "// decompile explicitement la machine a etats de la tache :\n",
+ "// `tache.GetAwaiter().GetResult()`. Meme consequence en production\n",
+ "// (deadlock sur un SynchronizationContext), mais un chemin syntaxique\n",
+ "// distinct qui justifie un analyseur dedie -- AGENTGUARD001 ne regarde\n",
+ "// que `.Result` / `.Wait()`. Diagnostic attendu sur la derniere ligne.\n",
+ "public static class AgentSyncOverAsync\n",
+ "{\n",
+ " // Genere par agent \"pour eviter un await\" : la tache est \"synchronisee\"\n",
+ " // en traversant manuellement la machine a etats. C'est exactement le\n",
+ " // defaut que AGENTGUARD005 attrape.\n",
+ " public static string ReponseSynchrone(string prompt)\n",
+ " {\n",
+ " return CallLlmAsync(prompt).GetAwaiter().GetResult(); // AGENTGUARD005\n",
+ " }\n",
+ "\n",
+ " private static async Task CallLlmAsync(string prompt)\n",
+ " {\n",
+ " await Task.Delay(50);\n",
+ " return $\"[LLM] {prompt}\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Sixieme terrain fautif (AGENTGUARD005b) : variante\n",
+ "// `ConfigureAwait(false).GetAwaiter().GetResult()`, ecappee au filtre\n",
+ "// semantique d'AGENTGUARD005 (le receiver du GetAwaiter y est de type\n",
+ "// `ConfiguredTaskAwaitable`, pas `Task`). L'agent genere cette forme en\n",
+ "// CROYANT que `ConfigureAwait(false)` \"rend ca safe\". Faux : ConfigureAwait\n",
+ "// reduit la capture du SynchronizationContext, mais le\n",
+ "// `.GetAwaiter().GetResult()` BLOQUE TOUJOURS le thread -- le\n",
+ "// sync-over-async reste entier. C'est precisement la confusion que la\n",
+ "// regle AGENTGUARD005b doit denoncer. Deux diagnostics attendus : un par\n",
+ "// ligne fautive (deux formes explicitement testees : `ConfigureAwait(false)`\n",
+ "// et `ConfigureAwait(true)`).\n",
+ "public static class AgentSyncOverAsyncConfigureAwait\n",
+ "{\n",
+ " // Forme \"rassurante\" classique : l'agent a lu sur Stack Overflow que\n",
+ " // ConfigureAwait(false) est une bonne pratique et l'a ajoute \"pour\n",
+ " // etre safe\". Le defaut est exactement le meme que la variante\n",
+ " // nue -- diagnostic AGENTGUARD005b attendu.\n",
+ " public static string ReponseSynchroneSafeFalse(string prompt)\n",
+ " {\n",
+ " return CallLlmAsync(prompt).ConfigureAwait(false).GetAwaiter().GetResult(); // AGENTGUARD005b\n",
+ " }\n",
+ "\n",
+ " // Forme `true` (explicite) -- meme defaut, l'analyseur doit le voir\n",
+ " // aussi (le literal n'est pas blanchi : seul compte le pattern).\n",
+ " public static string ReponseSynchroneSafeTrue(string prompt)\n",
+ " {\n",
+ " return CallLlmAsync(prompt).ConfigureAwait(true).GetAwaiter().GetResult(); // AGENTGUARD005b\n",
+ " }\n",
+ "\n",
+ " private static async Task CallLlmAsync(string prompt)\n",
+ " {\n",
+ " await Task.Delay(50);\n",
+ " return $\"[LLM] {prompt}\";\n",
+ " }\n",
+ "}\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -229,10 +496,10 @@
"id": "ag-07",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:02.326377Z",
- "iopub.status.busy": "2026-08-28T21:33:02.325352Z",
- "iopub.status.idle": "2026-08-28T21:33:02.380044Z",
- "shell.execute_reply": "2026-08-28T21:33:02.380044Z"
+ "iopub.execute_input": "2026-09-03T10:36:55.385464Z",
+ "iopub.status.busy": "2026-09-03T10:36:55.384895Z",
+ "iopub.status.idle": "2026-09-03T10:36:55.489040Z",
+ "shell.execute_reply": "2026-09-03T10:36:55.488528Z"
},
"papermill": {
"duration": 0.064056,
@@ -245,9 +512,71 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "using System.Collections.Immutable;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.Diagnostics;\n\nnamespace AgentGuard.Analyzers;\n\n/// \n/// AGENTGUARD001 : blocage synchrone d'une Task (.Result / .Wait()).\n///\n/// Pattern typique du code d'agent genere : appeler une tache asynchrone\n/// (appel LLM, streaming, canal) depuis du code synchrone en la bloquant.\n/// Le garde-fou vit DANS la compilation -- `dotnet build` rend le diagnostic\n/// sans aucun outil supplementaire (la these du notebook 06 de la serie Aspire,\n/// Epic #10473 axe Roslyn).\n/// \n[DiagnosticAnalyzer(LanguageNames.CSharp)]\npublic sealed class TaskResultBlockAnalyzer : DiagnosticAnalyzer\n{\n public const string DiagnosticId = \"AGENTGUARD001\";\n\n private static readonly DiagnosticDescriptor Rule = new(\n DiagnosticId,\n \"Blocage synchrone d'une Task\",\n \"Task bloquee de maniere synchrone ({0}) : deadlock potentiel en code d'agent\",\n \"Agentisme\",\n DiagnosticSeverity.Warning,\n isEnabledByDefault: true,\n description: \"Le code genere par agent qui bloque une Task via .Result/.Wait() expose au deadlock.\");\n\n public override ImmutableArray SupportedDiagnostics\n => ImmutableArray.Create(Rule);\n\n public override void Initialize(AnalysisContext context)\n {\n context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n context.EnableConcurrentExecution();\n // Le membre accede (.Result, .Wait) est un MemberAccessExpression --\n // on s'abonne a CE noeud syntaxique, pas a toute l'arborescence.\n context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression);\n }\n\n private static void AnalyzeNode(SyntaxNodeAnalysisContext ctx)\n {\n var node = (Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax)ctx.Node;\n\n // 1. Filtre syntaxique bon marche : le membre s'appelle Result ou Wait.\n if (node.Name is not { Identifier.ValueText: \"Result\" or \"Wait\" }) return;\n\n // 2. Filtre semantique : le membre appartient bien a Task / Task.\n // (Evince les faux positifs : un Result monadique, un .Wait()\n // de type custom.) C'est le modele semantique qui tranchera.\n if (ctx.SemanticModel.GetSymbolInfo(node).Symbol is not { } member) return;\n if (member.ContainingType is not INamedTypeSymbol ct\n || ct.MetadataName is not (\"Task\" or \"Task`1\")) return;\n\n ctx.ReportDiagnostic(Diagnostic.Create(\n Rule, node.GetLocation(), \".\" + node.Name.Identifier.ValueText));\n }\n}\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "using System.Collections.Immutable;\n",
+ "using Microsoft.CodeAnalysis;\n",
+ "using Microsoft.CodeAnalysis.CSharp;\n",
+ "using Microsoft.CodeAnalysis.Diagnostics;\n",
+ "\n",
+ "namespace AgentGuard.Analyzers;\n",
+ "\n",
+ "/// \n",
+ "/// AGENTGUARD001 : blocage synchrone d'une Task (.Result / .Wait()).\n",
+ "///\n",
+ "/// Pattern typique du code d'agent genere : appeler une tache asynchrone\n",
+ "/// (appel LLM, streaming, canal) depuis du code synchrone en la bloquant.\n",
+ "/// Le garde-fou vit DANS la compilation -- `dotnet build` rend le diagnostic\n",
+ "/// sans aucun outil supplementaire (la these du notebook 06 de la serie Aspire,\n",
+ "/// Epic #10473 axe Roslyn).\n",
+ "/// \n",
+ "[DiagnosticAnalyzer(LanguageNames.CSharp)]\n",
+ "public sealed class TaskResultBlockAnalyzer : DiagnosticAnalyzer\n",
+ "{\n",
+ " public const string DiagnosticId = \"AGENTGUARD001\";\n",
+ "\n",
+ " private static readonly DiagnosticDescriptor Rule = new(\n",
+ " DiagnosticId,\n",
+ " \"Blocage synchrone d'une Task\",\n",
+ " \"Task bloquee de maniere synchrone ({0}) : deadlock potentiel en code d'agent\",\n",
+ " \"Agentisme\",\n",
+ " DiagnosticSeverity.Warning,\n",
+ " isEnabledByDefault: true,\n",
+ " description: \"Le code genere par agent qui bloque une Task via .Result/.Wait() expose au deadlock.\");\n",
+ "\n",
+ " public override ImmutableArray SupportedDiagnostics\n",
+ " => ImmutableArray.Create(Rule);\n",
+ "\n",
+ " public override void Initialize(AnalysisContext context)\n",
+ " {\n",
+ " context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n",
+ " context.EnableConcurrentExecution();\n",
+ " // Le membre accede (.Result, .Wait) est un MemberAccessExpression --\n",
+ " // on s'abonne a CE noeud syntaxique, pas a toute l'arborescence.\n",
+ " context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression);\n",
+ " }\n",
+ "\n",
+ " private static void AnalyzeNode(SyntaxNodeAnalysisContext ctx)\n",
+ " {\n",
+ " var node = (Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax)ctx.Node;\n",
+ "\n",
+ " // 1. Filtre syntaxique bon marche : le membre s'appelle Result ou Wait.\n",
+ " if (node.Name is not { Identifier.ValueText: \"Result\" or \"Wait\" }) return;\n",
+ "\n",
+ " // 2. Filtre semantique : le membre appartient bien a Task / Task.\n",
+ " // (Evince les faux positifs : un Result monadique, un .Wait()\n",
+ " // de type custom.) C'est le modele semantique qui tranchera.\n",
+ " if (ctx.SemanticModel.GetSymbolInfo(node).Symbol is not { } member) return;\n",
+ " if (member.ContainingType is not INamedTypeSymbol ct\n",
+ " || ct.MetadataName is not (\"Task\" or \"Task`1\")) return;\n",
+ "\n",
+ " ctx.ReportDiagnostic(Diagnostic.Create(\n",
+ " Rule, node.GetLocation(), \".\" + node.Name.Identifier.ValueText));\n",
+ " }\n",
+ "}\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -294,18 +623,9 @@
"tags": []
},
"source": [
- "### Exercice 1 — Attraper la variante furtive `GetAwaiter().GetResult()`\n",
+ "### Exercice 1 -- Predire le verdict du Verifier sur le terrain a exemptions\n",
"\n",
- "Le développeur (ou l'agent) qui découvre l'avertissement sur `.Result` connaît souvent l'échappatoire : `task.GetAwaiter().GetResult()` bloque **exactement autant**, mais l'analyseur actuel ne la voit pas — il ne surveille que les accès directs `.Result`/`.Wait`.\n",
- "\n",
- "**Objectif** : étendre l'analyseur pour signaler cette variante.\n",
- "\n",
- "**Indices** :\n",
- "- `# Indice` : le nœud à surveiller devient `SyntaxKind.InvocationExpression` — `GetResult()` est une *invocation*, pas un simple accès.\n",
- "- `# Indice` : dans l'invocation, vérifier que le membre invoqué s'appelle `GetResult` **et** que son récepteur est lui-même l'invocation de `GetAwaiter` sur... un `Task` (étage sémantique inchangé).\n",
- "- `# Etape 1` : écrire un source de test contenant `t.GetAwaiter().GetResult()` et vérifier à la main que le Verifier (section suivante) le rend PROPRE aujourd'hui.\n",
- "- `# Etape 2` : ajouter l'abonnement et le filtre dans une copie de `TaskResultBlockAnalyzer`.\n",
- "- `# Etape 3` : recharger l'analyseur dans le projet Demo (`dotnet build`) et vérifier que la variante furtive rend AGENTGUARD001."
+ "L'analyseur AGENTGUARD005b est livre, ses exemptions sont posees explicitement. Le terrain `SyncOverAsyncConfigureAwaitValueTask.cs` materialise **trois cas d'exemption** -- tous les trois legitimes. **TODO etudiant** : avant d'executer la cellule Verifier, predire le verdict de chacun (rouge / propre), puis executer, puis **citer pour chaque cas le numero de la clause d'exemption dans `SyncOverAsyncConfigureAwaitAnalyzer.cs`** (le filtre syntaxique, le filtre semantique, ou l'absence de `GetResult`). Relier la clause citee au type de defense (anti-faux-positif)."
]
},
{
@@ -314,10 +634,10 @@
"id": "ag-10",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:02.455348Z",
- "iopub.status.busy": "2026-08-28T21:33:02.454797Z",
- "iopub.status.idle": "2026-08-28T21:33:02.512459Z",
- "shell.execute_reply": "2026-08-28T21:33:02.512459Z"
+ "iopub.execute_input": "2026-09-03T10:36:55.492352Z",
+ "iopub.status.busy": "2026-09-03T10:36:55.491734Z",
+ "iopub.status.idle": "2026-09-03T10:37:07.720064Z",
+ "shell.execute_reply": "2026-09-03T10:37:07.719565Z"
},
"papermill": {
"duration": 0.088129,
@@ -330,29 +650,59 @@
},
"outputs": [
{
+ "name": "stdout",
"output_type": "stream",
+ "text": [
+ "[SyncOverAsyncConfigureAwaitFautif.cs] VERDICT : 2 diagnostic(s) -- AGENTGUARD005b x2\r\n",
+ " AGENTGUARD005b @ 25:16 ConfigureAwait(False) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ " AGENTGUARD005b @ 32:16 ConfigureAwait(True) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ "[SyncOverAsyncConfigureAwaitValueTask.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "\r\n"
+ ]
+ },
+ {
"name": "stdout",
- "text": "Exercice a completer : etendre l'analyseur a GetAwaiter().GetResult()\r\n"
+ "output_type": "stream",
+ "text": [
+ "Exercice a completer : citer pour chaque cas la clause d'exemption gagnee\r\n"
+ ]
}
],
"source": [
- "// Exercice 1 a completer -- squelette de l'extension a ecrire dans une copie\n",
- "// de TaskResultBlockAnalyzer.cs. Le TODO marque le point d'insertion.\n",
+ "// Exercice 1 -- prediction AVANT execution.\n",
"//\n",
- "// public override void Initialize(AnalysisContext context)\n",
- "// {\n",
- "// context.RegisterSyntaxNodeAction(AnalyzeInvocation,\n",
- "// SyntaxKind.InvocationExpression); // TODO etudiant\n",
- "// }\n",
+ "// Le terrain `SyncOverAsyncConfigureAwaitValueTask.cs` porte trois\n",
+ "// cas. Pour CHACUN, predire le verdict AGENTGUARD005b (rouge / propre)\n",
+ "// PUIS executer le Verifier sur le terrain et confronter :\n",
"//\n",
- "// private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)\n",
- "// {\n",
- "// var inv = (InvocationExpressionSyntax)ctx.Node;\n",
- "// // TODO etudiant : membre == \"GetResult\" ?\n",
- "// // TODO etudiant : recepteur == invocation de \"GetAwaiter\" sur un Task ?\n",
- "// // TODO etudiant : ctx.ReportDiagnostic(...)\n",
- "// }\n",
- "Console.WriteLine(\"Exercice a completer : etendre l'analyseur a GetAwaiter().GetResult()\");"
+ "// var predits = new (string cas, string verdictAttendu, string clauseExemption)[]\n",
+ "// {\n",
+ "// (\"ValueTaskSync\", \"PROPRE\", \"filtre semantique : ValueTask n'est pas Task\"),\n",
+ "// (\"AvecCondition\", \"PROPRE\", \"filtre syntaxique : argument n'est pas un literal bool\"),\n",
+ "// (\"SansGetResult\", \"PROPRE\", \"filtre syntaxique : pas de GetResult dans la chaine\"),\n",
+ "// };\n",
+ "// foreach (var p in predits) Console.WriteLine($\"{p.cas,-15} attendu={p.verdictAttendu,-7} | {p.clauseExemption}\");\n",
+ "//\n",
+ "// L'analyseur distingue 3 systemes de defense orthogonaux. Une seule\n",
+ "// clause suffit a blanchir un cas -- ce qui rend les faux-positifs\n",
+ "// rares, mais pas gratuits : les nommer dans la reponse est la\n",
+ "// moitie de l'exercice.\n",
+ "\n",
+ "// Note : le Verifier exige args.Length >= 2 (Program.cs:13 retourne\n",
+ "// exit 2 + banner usage sinon). On lance donc la paire fautif/valueTask :\n",
+ "// la fautive produit 2 diagnostics AGENTGUARD005b (preuve que\n",
+ "// l'analyseur fonctionne sur ConfigureAwait(false)/(true).GetAwaiter()\n",
+ "// .GetResult()) ; la valueTask produit 3 diagnostics PROPRE (preuve\n",
+ "// des 3 clauses d'exemption : ValueTask != Task, literal non-bool,\n",
+ "// pas de GetResult dans la chaine). La confrontation pedagogique\n",
+ "// predire/executer/verifier est donc possible sur le terrain.\n",
+ "var verdictsValueTask = Shell.Run(here, \"dotnet\",\n",
+ " \"run --project AgentGuard.Verifier -- \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncConfigureAwaitFautif.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncConfigureAwaitValueTask.cs\");\n",
+ "Console.WriteLine(verdictsValueTask);\n",
+ "\n",
+ "Console.WriteLine(\"Exercice a completer : citer pour chaque cas la clause d'exemption gagnee\");\n"
]
},
{
@@ -387,10 +737,10 @@
"id": "ag-12",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:02.547787Z",
- "iopub.status.busy": "2026-08-28T21:33:02.546182Z",
- "iopub.status.idle": "2026-08-28T21:33:10.279594Z",
- "shell.execute_reply": "2026-08-28T21:33:10.279594Z"
+ "iopub.execute_input": "2026-09-03T10:37:07.723005Z",
+ "iopub.status.busy": "2026-09-03T10:37:07.722514Z",
+ "iopub.status.idle": "2026-09-03T10:37:11.944506Z",
+ "shell.execute_reply": "2026-09-03T10:37:11.944228Z"
},
"papermill": {
"duration": 7.741769,
@@ -403,9 +753,34 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "AgentGuard.Demo\\Program.cs(24,22): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(25,16): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(88,15): warning AGENTGUARD004: L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(55,30): warning AGENTGUARD002: La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(75,9): warning AGENTGUARD003: Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini [AgentGuard.Demo]\r\n\r\nLa génération a réussi.\r\n\r\nAgentGuard.Demo\\Program.cs(24,22): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(25,16): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(88,15): warning AGENTGUARD004: L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(55,30): warning AGENTGUARD002: La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort [AgentGuard.Demo]\r\nAgentGuard.Demo\\Program.cs(75,9): warning AGENTGUARD003: Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini [AgentGuard.Demo]\r\n 5 Avertissement(s)\r\n 0 Erreur(s)\r\n\r\nTemps écoulé 00:00:02.01\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "AgentGuard.Demo\\Program.cs(55,30): warning AGENTGUARD002: La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(111,16): warning AGENTGUARD005: GetAwaiter().GetResult() bloque une Task/Task`1 de maniere synchrone : remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(140,16): warning AGENTGUARD005b: ConfigureAwait(False) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(24,22): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(25,16): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(75,9): warning AGENTGUARD003: Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(147,16): warning AGENTGUARD005b: ConfigureAwait(True) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(88,15): warning AGENTGUARD004: L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante [AgentGuard.Demo]\r\n",
+ "\r\n",
+ "La génération a réussi.\r\n",
+ "\r\n",
+ "AgentGuard.Demo\\Program.cs(55,30): warning AGENTGUARD002: La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(111,16): warning AGENTGUARD005: GetAwaiter().GetResult() bloque une Task/Task`1 de maniere synchrone : remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(140,16): warning AGENTGUARD005b: ConfigureAwait(False) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(24,22): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(25,16): warning AGENTGUARD001: Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(75,9): warning AGENTGUARD003: Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(147,16): warning AGENTGUARD005b: ConfigureAwait(True) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await [AgentGuard.Demo]\r\n",
+ "AgentGuard.Demo\\Program.cs(88,15): warning AGENTGUARD004: L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante [AgentGuard.Demo]\r\n",
+ " 8 Avertissement(s)\r\n",
+ " 0 Erreur(s)\r\n",
+ "\r\n",
+ "Temps écoulé 00:00:03.30\r\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -442,7 +817,20 @@
},
"tags": []
},
- "source": "### Lecture du verdict\n\nLa sortie du build porte désormais **cinq avertissements**, un par défaut démontré :\n\n- `AGENTGUARD001 x2` — les deux `.Result` qui bloquent la lecture du canal puis l'appel LLM ;\n- `AGENTGUARD002 x1` — la méthode `async void SurveillerCanalAsync` hors gestionnaire d'événement ;\n- `AGENTGUARD003 x1` — le `Task.Run(...)` lancé comme énoncé autonome ;\n- `AGENTGUARD004 x1` — l'appel LLM omet le `CancellationToken` pourtant disponible et accepté par la signature cible.\n\nLes quatre analyseurs tirent dans le même build, sans configuration supplémentaire : chaque garde-fou ajouté au projet `AgentGuard.Analyzers` est automatiquement actif. Chacun nomme le fichier, la ligne, la colonne et le message complet. La dernière ligne — `0 Erreur(s)` — rappelle le choix de sévérité : le build réussit, le diagnostic est un garde-fou, pas un marteau.\n\n**Le point décisif** : cette sortie est produite par `dotnet build`, la commande la plus ordinaire du monde .NET. Aucune mention d'outil externe, aucune étape de lint. Le diagnostic est arrivé **avec la compilation elle-même** — c'est ce que Python ne peut pas offrir par construction, et que la section C détaille."
+ "source": [
+ "### Lecture du verdict\n",
+ "\n",
+ "La sortie du build porte désormais **cinq avertissements**, un par défaut démontré :\n",
+ "\n",
+ "- `AGENTGUARD001 x2` — les deux `.Result` qui bloquent la lecture du canal puis l'appel LLM ;\n",
+ "- `AGENTGUARD002 x1` — la méthode `async void SurveillerCanalAsync` hors gestionnaire d'événement ;\n",
+ "- `AGENTGUARD003 x1` — le `Task.Run(...)` lancé comme énoncé autonome ;\n",
+ "- `AGENTGUARD004 x1` — l'appel LLM omet le `CancellationToken` pourtant disponible et accepté par la signature cible.\n",
+ "\n",
+ "Les quatre analyseurs tirent dans le même build, sans configuration supplémentaire : chaque garde-fou ajouté au projet `AgentGuard.Analyzers` est automatiquement actif. Chacun nomme le fichier, la ligne, la colonne et le message complet. La dernière ligne — `0 Erreur(s)` — rappelle le choix de sévérité : le build réussit, le diagnostic est un garde-fou, pas un marteau.\n",
+ "\n",
+ "**Le point décisif** : cette sortie est produite par `dotnet build`, la commande la plus ordinaire du monde .NET. Aucune mention d'outil externe, aucune étape de lint. Le diagnostic est arrivé **avec la compilation elle-même** — c'est ce que Python ne peut pas offrir par construction, et que la section C détaille."
+ ]
},
{
"cell_type": "markdown",
@@ -471,10 +859,10 @@
"id": "ag-15",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:10.331547Z",
- "iopub.status.busy": "2026-08-28T21:33:10.331547Z",
- "iopub.status.idle": "2026-08-28T21:33:16.297443Z",
- "shell.execute_reply": "2026-08-28T21:33:16.296919Z"
+ "iopub.execute_input": "2026-09-03T10:37:11.946349Z",
+ "iopub.status.busy": "2026-09-03T10:37:11.946110Z",
+ "iopub.status.idle": "2026-09-03T10:37:18.831182Z",
+ "shell.execute_reply": "2026-09-03T10:37:18.830654Z"
},
"papermill": {
"duration": 5.977317,
@@ -487,9 +875,21 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "[Program.cs] VERDICT : 5 diagnostic(s) -- AGENTGUARD001 x2, AGENTGUARD002 x1, AGENTGUARD003 x1, AGENTGUARD004 x1\r\n AGENTGUARD001 @ 24:22 Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent\r\n AGENTGUARD001 @ 25:16 Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent\r\n AGENTGUARD002 @ 55:30 La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort\r\n AGENTGUARD003 @ 75:9 Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini\r\n AGENTGUARD004 @ 88:15 L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante\r\n[AgentWorkerCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "[Program.cs] VERDICT : 8 diagnostic(s) -- AGENTGUARD001 x2, AGENTGUARD002 x1, AGENTGUARD003 x1, AGENTGUARD004 x1, AGENTGUARD005 x1, AGENTGUARD005b x2\r\n",
+ " AGENTGUARD001 @ 24:22 Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent\r\n",
+ " AGENTGUARD001 @ 25:16 Task bloquee de maniere synchrone (.Result) : deadlock potentiel en code d'agent\r\n",
+ " AGENTGUARD002 @ 55:30 La methode async void 'SurveillerCanalAsync' echappe a toute attente -- exceptions non observees, process mort\r\n",
+ " AGENTGUARD003 @ 75:9 Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini\r\n",
+ " AGENTGUARD004 @ 88:15 L'appel à 'Task AgentCancellation.CallLlmAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante\r\n",
+ " AGENTGUARD005 @ 111:16 GetAwaiter().GetResult() bloque une Task/Task`1 de maniere synchrone : remplacer par await\r\n",
+ " AGENTGUARD005b @ 140:16 ConfigureAwait(False) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ " AGENTGUARD005b @ 147:16 ConfigureAwait(True) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ "[AgentWorkerCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -511,7 +911,16 @@
},
"tags": []
},
- "source": "### Lecture des verdicts\n\nDeux lignes de verdict, chacune adossée à son fichier :\n\n- `Program.cs` (le terrain fautif) : **5 diagnostics** — `AGENTGUARD001 x2`, puis un diagnostic pour chacun des garde-fous `002`, `003` et `004` ;\n- `AgentWorkerCorrige.cs` (la version corrigée) : **PROPRE**, aucun garde-fou déclenché — son `CancellationToken` voyage déjà de bout en bout.\n\n**Deux canaux, un seul moteur** : les analyseurs sont identiques ; ce qui change, c'est qui les héberge — MSBuild pour le canal build, l'API pour le canal tests/IDE. Un test d'intégration continue peut faire exactement ce que fait ce Verifier : compiler les sources générés par l'agent et exiger zéro diagnostic AgentGuard. Le garde-fou devient alors **exécutable en CI sur du code qui n'existe même pas sous forme de projet** — génération, compilation en mémoire, verdict, puis conservation ou régénération."
+ "source": [
+ "### Lecture des verdicts\n",
+ "\n",
+ "Deux lignes de verdict, chacune adossée à son fichier :\n",
+ "\n",
+ "- `Program.cs` (le terrain fautif) : **5 diagnostics** — `AGENTGUARD001 x2`, puis un diagnostic pour chacun des garde-fous `002`, `003` et `004` ;\n",
+ "- `AgentWorkerCorrige.cs` (la version corrigée) : **PROPRE**, aucun garde-fou déclenché — son `CancellationToken` voyage déjà de bout en bout.\n",
+ "\n",
+ "**Deux canaux, un seul moteur** : les analyseurs sont identiques ; ce qui change, c'est qui les héberge — MSBuild pour le canal build, l'API pour le canal tests/IDE. Un test d'intégration continue peut faire exactement ce que fait ce Verifier : compiler les sources générés par l'agent et exiger zéro diagnostic AgentGuard. Le garde-fou devient alors **exécutable en CI sur du code qui n'existe même pas sous forme de projet** — génération, compilation en mémoire, verdict, puis conservation ou régénération."
+ ]
},
{
"cell_type": "code",
@@ -519,10 +928,10 @@
"id": "ag-17",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:16.327846Z",
- "iopub.status.busy": "2026-08-28T21:33:16.326829Z",
- "iopub.status.idle": "2026-08-28T21:33:16.387155Z",
- "shell.execute_reply": "2026-08-28T21:33:16.386647Z"
+ "iopub.execute_input": "2026-09-03T10:37:18.833896Z",
+ "iopub.status.busy": "2026-09-03T10:37:18.833409Z",
+ "iopub.status.idle": "2026-09-03T10:37:18.889854Z",
+ "shell.execute_reply": "2026-09-03T10:37:18.889333Z"
},
"papermill": {
"duration": 0.068804,
@@ -535,9 +944,38 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "// Version corrigee du terrain fautif (AgentGuard.Demo/Program.cs) :\n// le worker attend la tache au lieu de la bloquer. C'est la version\n// attendue \"propre\" au verdict du Verifier.\n\nusing System;\nusing System.Threading;\nusing System.Threading.Channels;\nusing System.Threading.Tasks;\n\npublic static class AgentWorkerCorrige\n{\n // Le fix : async/await de bout en bout. Le thread rend la main pendant\n // l'attente au lieu de se bloquer -- plus de deadlock possible, et le\n // pipeline reste fluide sous charge.\n public static async Task TranslateAsync(\n ChannelReader inbound, CancellationToken ct = default)\n {\n var prompt = await inbound.ReadAsync(ct);\n return await CallLlmAsync(prompt, ct);\n }\n\n private static async Task CallLlmAsync(string prompt, CancellationToken ct)\n {\n await Task.Delay(50, ct); // simule la latence de l'appel LLM\n return $\"[LLM] {prompt}\";\n }\n}\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "// Version corrigee du terrain fautif (AgentGuard.Demo/Program.cs) :\n",
+ "// le worker attend la tache au lieu de la bloquer. C'est la version\n",
+ "// attendue \"propre\" au verdict du Verifier.\n",
+ "\n",
+ "using System;\n",
+ "using System.Threading;\n",
+ "using System.Threading.Channels;\n",
+ "using System.Threading.Tasks;\n",
+ "\n",
+ "public static class AgentWorkerCorrige\n",
+ "{\n",
+ " // Le fix : async/await de bout en bout. Le thread rend la main pendant\n",
+ " // l'attente au lieu de se bloquer -- plus de deadlock possible, et le\n",
+ " // pipeline reste fluide sous charge.\n",
+ " public static async Task TranslateAsync(\n",
+ " ChannelReader inbound, CancellationToken ct = default)\n",
+ " {\n",
+ " var prompt = await inbound.ReadAsync(ct);\n",
+ " return await CallLlmAsync(prompt, ct);\n",
+ " }\n",
+ "\n",
+ " private static async Task CallLlmAsync(string prompt, CancellationToken ct)\n",
+ " {\n",
+ " await Task.Delay(50, ct); // simule la latence de l'appel LLM\n",
+ " return $\"[LLM] {prompt}\";\n",
+ " }\n",
+ "}\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -605,10 +1043,10 @@
"id": "ag-20",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:16.431954Z",
- "iopub.status.busy": "2026-08-28T21:33:16.431445Z",
- "iopub.status.idle": "2026-08-28T21:33:16.713064Z",
- "shell.execute_reply": "2026-08-28T21:33:16.711435Z"
+ "iopub.execute_input": "2026-09-03T10:37:18.892709Z",
+ "iopub.status.busy": "2026-09-03T10:37:18.892244Z",
+ "iopub.status.idle": "2026-09-03T10:37:19.176185Z",
+ "shell.execute_reply": "2026-09-03T10:37:19.175789Z"
},
"papermill": {
"duration": 0.291289,
@@ -621,14 +1059,18 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "Outcome.Result = aucun blocage ici\r\n"
+ "output_type": "stream",
+ "text": [
+ "Outcome.Result = aucun blocage ici\r\n"
+ ]
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "Exercice a completer : passer ce type au Verifier et nommer la ligne de l'exemption\r\n"
+ "output_type": "stream",
+ "text": [
+ "Exercice a completer : passer ce type au Verifier et nommer la ligne de l'exemption\r\n"
+ ]
}
],
"source": [
@@ -670,10 +1112,10 @@
"id": "ag-22",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:16.741786Z",
- "iopub.status.busy": "2026-08-28T21:33:16.740778Z",
- "iopub.status.idle": "2026-08-28T21:33:16.881159Z",
- "shell.execute_reply": "2026-08-28T21:33:16.881159Z"
+ "iopub.execute_input": "2026-09-03T10:37:19.178907Z",
+ "iopub.status.busy": "2026-09-03T10:37:19.178445Z",
+ "iopub.status.idle": "2026-09-03T10:37:19.513814Z",
+ "shell.execute_reply": "2026-09-03T10:37:19.513326Z"
},
"papermill": {
"duration": 0.148598,
@@ -686,14 +1128,19 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "Information : impossible de trouver des fichiers pour le(s) modèle(s) spécifié(s).\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "Information : impossible de trouver des fichiers pour le(s) modèle(s) spécifié(s).\r\n",
+ "\r\n"
+ ]
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "-> ruff ABSENT : en Python le garde-fou est un outil a installer soi-meme\r\n"
+ "output_type": "stream",
+ "text": [
+ "-> ruff ABSENT : en Python le garde-fou est un outil a installer soi-meme\r\n"
+ ]
}
],
"source": [
@@ -736,6 +1183,113 @@
"La nuance honnête : l'écosystème Python compense par la **richesse des règles prêtes à l'emploi** (des centaines, maintenues par la communauté) là où notre analyseur est une règle maison de 40 lignes. Le déplacement profond n'est pas la quantité mais **le moment** : en .NET, le diagnostic est *constitutif* de la chaîne qui produit le binaire — impossible de compiler sans passer devant le garde-fou."
]
},
+ {
+ "cell_type": "markdown",
+ "id": "aae1ee96",
+ "metadata": {},
+ "source": [
+ "## D1 -- AGENTGUARD005 livre : sync-over-async, deux variantes\n",
+ "\n",
+ "Le code genere par agent attrape souvent la variante **`.GetAwaiter().GetResult()`** de la famille sync-over-async -- exactement le meme defaut que `.Result` (AGENTGUARD001), mais emprunte par un chemin syntaxique distinct. L'agent voit un appel de methode ordinaire ; il ne se doute pas qu'il traverse la machine a etats d'une `Task` et bloque son thread.\n",
+ "\n",
+ "Deux analyseurs sont livres dans `AgentGuard.Analyzers/` :\n",
+ "\n",
+ "- **`SyncOverAsyncAnalyzer`** (AGENTGUARD005) : filtre syntaxique (`GetAwaiter().GetResult()`) **plus** filtre semantique (le receiver du `GetAwaiter()` est-il `System.Threading.Tasks.Task` ou `Task` ?). Les awaiters personalises et les `ValueTask` sont exemptes par construction.\n",
+ "- **`SyncOverAsyncConfigureAwaitAnalyzer`** (AGENTGUARD005b) : variante pedagogique dediee, capture `tache.ConfigureAwait(bool).GetAwaiter().GetResult()`. Le message diagnostique explique **pourquoi `ConfigureAwait(false)` ne sauve pas** : il reduit la capture du `SynchronizationContext`, mais `.GetAwaiter().GetResult()` bloque toujours le thread. Le sync-over-async reste entier."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bd0f0626",
+ "metadata": {},
+ "source": [
+ "**D1 -- les deux analyseurs, cote a cote. Sortie observee :**\n",
+ "\n",
+ "```\n",
+ "// --- borne semantique partagee (AGENTGUARD005 + 005b) ---\n",
+ "// MetadataName is not (\"Task\" or \"Task`1\") -> un awaiter custom n'est pas Task\n",
+ "\n",
+ "// --- pivot distinctif de AGENTGUARD005b ---\n",
+ "// Le troisieme filtre de AGENTGUARD005b exige un literal bool :\n",
+ "// if (cev.Arguments[0].Expression is not LiteralExpressionSyntax)\n",
+ "// return; -- pas un literal -> pas signale\n",
+ "\n",
+ "// --- message diagnostique de AGENTGUARD005b ---\n",
+ "// Format string du Rule :\n",
+ "// \"ConfigureAwait({0}) ne protege pas du sync-over-async :\"\n",
+ "\n",
+ "// --- diagnostic_id distincts ---\n",
+ "// AGENTGUARD005 : SyncOverAsyncAnalyzer.cs\n",
+ "// AGENTGUARD005b : SyncOverAsyncConfigureAwaitAnalyzer.cs\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "8d4eecd3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-03T10:37:19.516581Z",
+ "iopub.status.busy": "2026-09-03T10:37:19.516144Z",
+ "iopub.status.idle": "2026-09-03T10:37:26.922071Z",
+ "shell.execute_reply": "2026-09-03T10:37:26.921636Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[SyncOverAsyncFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD005 x1\r\n",
+ " AGENTGUARD005 @ 21:9 GetAwaiter().GetResult() bloque une Task/Task de maniere synchrone : remplacer par await\r\n",
+ "[SyncOverAsyncCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[SyncOverAsyncGenericFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD005 x1\r\n",
+ " AGENTGUARD005 @ 21:16 GetAwaiter().GetResult() bloque une Task/Task`1 de maniere synchrone : remplacer par await\r\n",
+ "[SyncOverAsyncSansGetResult.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[SyncOverAsyncAwaiterPersonnalise.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[SyncOverAsyncConfigureAwaitFautif.cs] VERDICT : 2 diagnostic(s) -- AGENTGUARD005b x2\r\n",
+ " AGENTGUARD005b @ 25:16 ConfigureAwait(False) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ " AGENTGUARD005b @ 32:16 ConfigureAwait(True) ne protege pas du sync-over-async : .GetAwaiter().GetResult() bloque toujours le thread, remplacer par await\r\n",
+ "\r\n"
+ ]
+ }
+ ],
+ "source": [
+ "// D1 -- verdicts reels sur les 6 terrains SyncOverAsync committes.\n",
+ "// 5 terrains AGENTGUARD005 + 1 terrain AGENTGUARD005b. On observe le\n",
+ "// contraste entre les 3 rouges (fautifs) et les 3 propres (corriges /\n",
+ "// exemptes par le filtre semantique ou syntaxique).\n",
+ "\n",
+ "var verdicts005 = Shell.Run(here, \"dotnet\",\n",
+ " \"run --project AgentGuard.Verifier -- \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncFautif.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncCorrige.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncGenericFautif.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncSansGetResult.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncAwaiterPersonnalise.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/SyncOverAsyncConfigureAwaitFautif.cs\");\n",
+ "Console.WriteLine(verdicts005);\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1bdc01fd",
+ "metadata": {},
+ "source": [
+ "### Lecture des verdicts\n",
+ "\n",
+ "Les trois rouges sont tous du meme defaut (`GetAwaiter().GetResult()` synchrone sur une `Task`), portes par trois formes differentes : **non-generique** (`SyncOverAsyncFautif`), **generique** (`SyncOverAsyncGenericFautif` -- `Task`), **avec `ConfigureAwait`** (`SyncOverAsyncConfigureAwaitFautif` -- qui declenche AGENTGUARD005b mais pas AGENTGUARD005, parce que le receiver du `GetAwaiter` est `ConfiguredTaskAwaitable`, pas `Task`).\n",
+ "\n",
+ "Les trois propres relevent des trois clauses d'exemption :\n",
+ "\n",
+ "- **`SyncOverAsyncCorrige`** : methode `async` + `await`. Plus de chaine fautive -- l'analyseur laisse passer.\n",
+ "- **`SyncOverAsyncSansGetResult`** : la chaine accede a `IsCompleted`, pas a `GetResult()`. Filtre syntaxique du membre invoque -> l'analyseur ne se declenche pas.\n",
+ "- **`SyncOverAsyncAwaiterPersonnalise`** : `MonAwaitable.GetAwaiter().GetResult()`. Le filtre semantique verifie que le receiver du `GetAwaiter()` est `System.Threading.Tasks.Task` ; ici c'est `MonAwaitable`. Pas de diagnostic.\n",
+ "\n",
+ "**Le message d'AGENTGUARD005b est explicite** (contrairement a AGENTGUARD001) : il dit *\"`ConfigureAwait(false)` ne protege pas du sync-over-async\"*. C'est la valeur d'un analyseur dedie plutot qu'une regle : expliquer, pas seulement hurler."
+ ]
+ },
{
"cell_type": "markdown",
"id": "ag-24",
@@ -786,14 +1340,14 @@
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 11,
"id": "ag-26",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:16.931527Z",
- "iopub.status.busy": "2026-08-28T21:33:16.931005Z",
- "iopub.status.idle": "2026-08-28T21:33:21.348232Z",
- "shell.execute_reply": "2026-08-28T21:33:21.348232Z"
+ "iopub.execute_input": "2026-09-03T10:37:26.924667Z",
+ "iopub.status.busy": "2026-09-03T10:37:26.924205Z",
+ "iopub.status.idle": "2026-09-03T10:37:34.274339Z",
+ "shell.execute_reply": "2026-09-03T10:37:34.273890Z"
},
"papermill": {
"duration": 4.425443,
@@ -806,24 +1360,49 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "// --- l'exemption, extraite de AsyncVoidAnalyzer.cs ---\r\n"
+ "output_type": "stream",
+ "text": [
+ "// --- l'exemption, extraite de AsyncVoidAnalyzer.cs ---\r\n"
+ ]
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "private static bool EstHandlerEvenement(MethodDeclarationSyntax method, SyntaxNodeAnalysisContext ctx)\n {\n var parameters = method.ParameterList.Parameters;\n if (parameters.Count < 2) return false;\n\n // Premier parametre exactement object (le \"sender\").\n var senderType = ctx.SemanticModel.GetTypeInfo(parameters[0].Type).Type;\n if (senderType?.SpecialType != SpecialType.System_Object) return false;\n\n // Second parametre derive de System.EventArgs (ou l'est lui-meme).\n var argsType = ctx.SemanticModel.GetTypeInfo(parameters[1].Type).Type;\n if (argsType is null || argsType.TypeKind == TypeKind.Error) return false;\n return EstEventArgsOuDerive(argsType, ctx.Compilation);\n }\r\n"
+ "output_type": "stream",
+ "text": [
+ "private static bool EstHandlerEvenement(MethodDeclarationSyntax method, SyntaxNodeAnalysisContext ctx)\n",
+ " {\n",
+ " var parameters = method.ParameterList.Parameters;\n",
+ " if (parameters.Count < 2) return false;\n",
+ "\n",
+ " // Premier parametre exactement object (le \"sender\").\n",
+ " var senderType = ctx.SemanticModel.GetTypeInfo(parameters[0].Type).Type;\n",
+ " if (senderType?.SpecialType != SpecialType.System_Object) return false;\n",
+ "\n",
+ " // Second parametre derive de System.EventArgs (ou l'est lui-meme).\n",
+ " var argsType = ctx.SemanticModel.GetTypeInfo(parameters[1].Type).Type;\n",
+ " if (argsType is null || argsType.TypeKind == TypeKind.Error) return false;\n",
+ " return EstEventArgsOuDerive(argsType, ctx.Compilation);\n",
+ " }\r\n"
+ ]
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "\r\n"
+ "output_type": "stream",
+ "text": [
+ "\r\n"
+ ]
},
{
- "output_type": "stream",
"name": "stdout",
- "text": "[AsyncVoidFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD002 x1\r\n AGENTGUARD002 @ 13:30 La methode async void 'TraiterCommande' echappe a toute attente -- exceptions non observees, process mort\r\n[AsyncVoidCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[AsyncVoidHandlerExempt.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "[AsyncVoidFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD002 x1\r\n",
+ " AGENTGUARD002 @ 13:30 La methode async void 'TraiterCommande' echappe a toute attente -- exceptions non observees, process mort\r\n",
+ "[AsyncVoidCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[AsyncVoidHandlerExempt.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -899,14 +1478,14 @@
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 12,
"id": "ft06_exo3_v2",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:21.391989Z",
- "iopub.status.busy": "2026-08-28T21:33:21.391989Z",
- "iopub.status.idle": "2026-08-28T21:33:21.424306Z",
- "shell.execute_reply": "2026-08-28T21:33:21.423764Z"
+ "iopub.execute_input": "2026-09-03T10:37:34.277213Z",
+ "iopub.status.busy": "2026-09-03T10:37:34.276601Z",
+ "iopub.status.idle": "2026-09-03T10:37:34.381453Z",
+ "shell.execute_reply": "2026-09-03T10:37:34.380999Z"
},
"papermill": {
"duration": 0.042088,
@@ -919,9 +1498,100 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "using System.Collections.Immutable;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\nusing Microsoft.CodeAnalysis.Diagnostics;\n\nnamespace AgentGuard.Analyzers;\n\n/// \n/// AGENTGUARD003 : invocation nue de Task.Run, tache non observee.\n///\n/// Troisieme pattern typique du code genere par agent : ecrire\n/// `Task.Run(() => Travail())` comme enonce autonome. La signature est\n/// honnete (la methode rend une Task, pas void), MAIS la tache resultante\n/// n'est ni attendue (await), ni affectee a une variable, ni retournee,\n/// ni explicitement ignoree via discard (`_ =`). Elle s'execute en arriere-\n/// plan ; ses exceptions ne sont observees par personne. A la finalisation\n/// d'une telle tache fautive, le runtime declenche\n/// `TaskScheduler.UnobservedTaskException`, un evenement qui porte une\n/// `AggregateException` collectant les exceptions internes -- le defaut\n/// (.NET 4.5+) est d'absorber l'evenement et de laisser le process vivre,\n/// mais ce comportement est configurable et n'est pas garanti.\n///\n/// Formes LEGITIMES (a ne PAS signaler) :\n/// - `await Task.Run(...)` -- tache observee\n/// - `var t = Task.Run(...)` -- tache recuperee (composee ou attendue plus tard)\n/// - `_ = Task.Run(...)` -- discard explicite (assume, volontaire)\n/// - `return Task.Run(...)` -- tache retournee a l'appelant\n/// - homonyme custom (autre type, autre signature) -- filtre semantique\n/// \n[DiagnosticAnalyzer(LanguageNames.CSharp)]\npublic sealed class TaskRunFireAnalyzer : DiagnosticAnalyzer\n{\n public const string DiagnosticId = \"AGENTGUARD003\";\n\n private static readonly DiagnosticDescriptor Rule = new(\n DiagnosticId,\n \"Task.Run feu, tache non observee\",\n \"Task.Run '{0}' lance une tache non observee -- exceptions perdues, comportement indefini\",\n \"Agentisme\",\n DiagnosticSeverity.Warning,\n isEnabledByDefault: true,\n description: \"Une invocation nue de Task.Run execute la tache en arriere-plan ; ses exceptions ne sont observees par personne. Utiliser await, affecter a une variable, retourner ou discarder explicitement (_ =).\");\n\n public override ImmutableArray SupportedDiagnostics\n => ImmutableArray.Create(Rule);\n\n public override void Initialize(AnalysisContext context)\n {\n context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n context.EnableConcurrentExecution();\n // On s'abonne aux INVOCATIONS (le noeud Task.Run(...)). Le diagnostic\n // porte sur l'invocation, pas sur un membre -- le membre ici est un\n // IdentifierName (Run), pas un MemberAccessExpression, donc on ne peut\n // pas reutiliser la strategie d'AGENTGUARD001.\n context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);\n }\n\n private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)\n {\n var inv = (InvocationExpressionSyntax)ctx.Node;\n\n // 1. Filtre semantique : la methode invoquee est bien Task.Run.\n // Reutilise le pattern d'AGENTGUARD001 : resoudre le symbole et\n // verifier que la ContainingType est Task (non-generique).\n // Evince les methodes homonymes : un type custom `MonService.Run`\n // ne doit PAS declencher AGENTGUARD003.\n if (ctx.SemanticModel.GetSymbolInfo(inv).Symbol is not IMethodSymbol method) return;\n if (method.ContainingType is not INamedTypeSymbol ct\n || ct.MetadataName is not \"Task\"\n || ct.ContainingNamespace?.ToDisplayString() != \"System.Threading.Tasks\") return;\n if (method.MetadataName is not \"Run\") return;\n\n // 2. Filtre syntaxique : la seule forme signalee est l'ExpressionStatement\n // nu (l'invocation est l'integralite de l'enonce). Les autres formes\n // -- await, affectation, discard, return, argument d'une autre\n // invocation -- ne sont JAMAIS signalees.\n //\n // Cas particulier `_ = Task.Run(...)` : Roslyn represente le discard\n // comme un AssignmentExpression avec Left = IdentifierName(\"_\"),\n // donc le parent n'est PAS un ExpressionStatement nu -- il est\n // rattrape par le filtre \"n'est pas ExpressionStatement\" et tombe\n // naturellement dans la branche exempt. Le test terrain le verifie.\n if (inv.Parent is not ExpressionStatementSyntax) return;\n\n ctx.ReportDiagnostic(Diagnostic.Create(\n Rule, inv.GetLocation(), inv.ToString()));\n }\n}\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "using System.Collections.Immutable;\n",
+ "using Microsoft.CodeAnalysis;\n",
+ "using Microsoft.CodeAnalysis.CSharp;\n",
+ "using Microsoft.CodeAnalysis.CSharp.Syntax;\n",
+ "using Microsoft.CodeAnalysis.Diagnostics;\n",
+ "\n",
+ "namespace AgentGuard.Analyzers;\n",
+ "\n",
+ "/// \n",
+ "/// AGENTGUARD003 : invocation nue de Task.Run, tache non observee.\n",
+ "///\n",
+ "/// Troisieme pattern typique du code genere par agent : ecrire\n",
+ "/// `Task.Run(() => Travail())` comme enonce autonome. La signature est\n",
+ "/// honnete (la methode rend une Task, pas void), MAIS la tache resultante\n",
+ "/// n'est ni attendue (await), ni affectee a une variable, ni retournee,\n",
+ "/// ni explicitement ignoree via discard (`_ =`). Elle s'execute en arriere-\n",
+ "/// plan ; ses exceptions ne sont observees par personne. A la finalisation\n",
+ "/// d'une telle tache fautive, le runtime declenche\n",
+ "/// `TaskScheduler.UnobservedTaskException`, un evenement qui porte une\n",
+ "/// `AggregateException` collectant les exceptions internes -- le defaut\n",
+ "/// (.NET 4.5+) est d'absorber l'evenement et de laisser le process vivre,\n",
+ "/// mais ce comportement est configurable et n'est pas garanti.\n",
+ "///\n",
+ "/// Formes LEGITIMES (a ne PAS signaler) :\n",
+ "/// - `await Task.Run(...)` -- tache observee\n",
+ "/// - `var t = Task.Run(...)` -- tache recuperee (composee ou attendue plus tard)\n",
+ "/// - `_ = Task.Run(...)` -- discard explicite (assume, volontaire)\n",
+ "/// - `return Task.Run(...)` -- tache retournee a l'appelant\n",
+ "/// - homonyme custom (autre type, autre signature) -- filtre semantique\n",
+ "/// \n",
+ "[DiagnosticAnalyzer(LanguageNames.CSharp)]\n",
+ "public sealed class TaskRunFireAnalyzer : DiagnosticAnalyzer\n",
+ "{\n",
+ " public const string DiagnosticId = \"AGENTGUARD003\";\n",
+ "\n",
+ " private static readonly DiagnosticDescriptor Rule = new(\n",
+ " DiagnosticId,\n",
+ " \"Task.Run feu, tache non observee\",\n",
+ " \"Task.Run '{0}' lance une tache non observee -- exceptions perdues, comportement indefini\",\n",
+ " \"Agentisme\",\n",
+ " DiagnosticSeverity.Warning,\n",
+ " isEnabledByDefault: true,\n",
+ " description: \"Une invocation nue de Task.Run execute la tache en arriere-plan ; ses exceptions ne sont observees par personne. Utiliser await, affecter a une variable, retourner ou discarder explicitement (_ =).\");\n",
+ "\n",
+ " public override ImmutableArray SupportedDiagnostics\n",
+ " => ImmutableArray.Create(Rule);\n",
+ "\n",
+ " public override void Initialize(AnalysisContext context)\n",
+ " {\n",
+ " context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n",
+ " context.EnableConcurrentExecution();\n",
+ " // On s'abonne aux INVOCATIONS (le noeud Task.Run(...)). Le diagnostic\n",
+ " // porte sur l'invocation, pas sur un membre -- le membre ici est un\n",
+ " // IdentifierName (Run), pas un MemberAccessExpression, donc on ne peut\n",
+ " // pas reutiliser la strategie d'AGENTGUARD001.\n",
+ " context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);\n",
+ " }\n",
+ "\n",
+ " private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx)\n",
+ " {\n",
+ " var inv = (InvocationExpressionSyntax)ctx.Node;\n",
+ "\n",
+ " // 1. Filtre semantique : la methode invoquee est bien Task.Run.\n",
+ " // Reutilise le pattern d'AGENTGUARD001 : resoudre le symbole et\n",
+ " // verifier que la ContainingType est Task (non-generique).\n",
+ " // Evince les methodes homonymes : un type custom `MonService.Run`\n",
+ " // ne doit PAS declencher AGENTGUARD003.\n",
+ " if (ctx.SemanticModel.GetSymbolInfo(inv).Symbol is not IMethodSymbol method) return;\n",
+ " if (method.ContainingType is not INamedTypeSymbol ct\n",
+ " || ct.MetadataName is not \"Task\"\n",
+ " || ct.ContainingNamespace?.ToDisplayString() != \"System.Threading.Tasks\") return;\n",
+ " if (method.MetadataName is not \"Run\") return;\n",
+ "\n",
+ " // 2. Filtre syntaxique : la seule forme signalee est l'ExpressionStatement\n",
+ " // nu (l'invocation est l'integralite de l'enonce). Les autres formes\n",
+ " // -- await, affectation, discard, return, argument d'une autre\n",
+ " // invocation -- ne sont JAMAIS signalees.\n",
+ " //\n",
+ " // Cas particulier `_ = Task.Run(...)` : Roslyn represente le discard\n",
+ " // comme un AssignmentExpression avec Left = IdentifierName(\"_\"),\n",
+ " // donc le parent n'est PAS un ExpressionStatement nu -- il est\n",
+ " // rattrape par le filtre \"n'est pas ExpressionStatement\" et tombe\n",
+ " // naturellement dans la branche exempt. Le test terrain le verifie.\n",
+ " if (inv.Parent is not ExpressionStatementSyntax) return;\n",
+ "\n",
+ " ctx.ReportDiagnostic(Diagnostic.Create(\n",
+ " Rule, inv.GetLocation(), inv.ToString()));\n",
+ " }\n",
+ "}\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -947,7 +1617,23 @@
},
"tags": []
},
- "source": "## Conclusion\n\nCe notebook a démontré, exécution à l'appui :\n\n- quatre **analyseurs Roslyn réels** attrapent des défauts distincts du code d'agent généré : blocage synchrone, `async void`, `Task.Run` non observé et perte de propagation d'un `CancellationToken` ;\n- sur le **canal build**, `dotnet build` rend `AGENTGUARD001/002/003/004` sans outil supplémentaire — la thèse DANS-la-compilation ;\n- sur le **canal API**, le `Verifier` compile les terrains fautifs, corrigés et homonymes, puis rend les mêmes diagnostics que l'IDE et MSBuild ;\n- chaque analyseur combine un périmètre précis avec des exemptions exécutées, plutôt qu'une heuristique sur le nom d'une méthode ou d'un paramètre ;\n- le contraste Python reste un déplacement de moment : là où ruff exige adoption et activation par règle, l'analyseur .NET est constitutif de la chaîne de build.\n\nLes exercices restent non résolus et étendent les mêmes filtres sémantiques : `GetAwaiter().GetResult()`, l'exemption `Result`, `Task.Factory.StartNew` et, désormais, la portée d'un token dans une fonction locale.\n\n**Pour aller plus loin** : la série Aspire fournit le terrain d'agent ; l'Epic #10473 tient la parité Python/C# des garde-fous ; et la documentation Roslyn `DiagnosticAnalyzer` couvre les code fixers et les tests (`Microsoft.CodeAnalysis.Analyzer.Testing`).\n\nSee #10473"
+ "source": [
+ "## Conclusion\n",
+ "\n",
+ "Ce notebook a démontré, exécution à l'appui :\n",
+ "\n",
+ "- quatre **analyseurs Roslyn réels** attrapent des défauts distincts du code d'agent généré : blocage synchrone, `async void`, `Task.Run` non observé et perte de propagation d'un `CancellationToken` ;\n",
+ "- sur le **canal build**, `dotnet build` rend `AGENTGUARD001/002/003/004` sans outil supplémentaire — la thèse DANS-la-compilation ;\n",
+ "- sur le **canal API**, le `Verifier` compile les terrains fautifs, corrigés et homonymes, puis rend les mêmes diagnostics que l'IDE et MSBuild ;\n",
+ "- chaque analyseur combine un périmètre précis avec des exemptions exécutées, plutôt qu'une heuristique sur le nom d'une méthode ou d'un paramètre ;\n",
+ "- le contraste Python reste un déplacement de moment : là où ruff exige adoption et activation par règle, l'analyseur .NET est constitutif de la chaîne de build.\n",
+ "\n",
+ "Les exercices restent non résolus et étendent les mêmes filtres sémantiques : `GetAwaiter().GetResult()`, l'exemption `Result`, `Task.Factory.StartNew` et, désormais, la portée d'un token dans une fonction locale.\n",
+ "\n",
+ "**Pour aller plus loin** : la série Aspire fournit le terrain d'agent ; l'Epic #10473 tient la parité Python/C# des garde-fous ; et la documentation Roslyn `DiagnosticAnalyzer` couvre les code fixers et les tests (`Microsoft.CodeAnalysis.Analyzer.Testing`).\n",
+ "\n",
+ "See #10473"
+ ]
},
{
"cell_type": "markdown",
@@ -968,14 +1654,14 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 13,
"id": "a5ea7c5b",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:21.466983Z",
- "iopub.status.busy": "2026-08-28T21:33:21.466983Z",
- "iopub.status.idle": "2026-08-28T21:33:25.377842Z",
- "shell.execute_reply": "2026-08-28T21:33:25.377842Z"
+ "iopub.execute_input": "2026-09-03T10:37:34.384643Z",
+ "iopub.status.busy": "2026-09-03T10:37:34.384125Z",
+ "iopub.status.idle": "2026-09-03T10:37:41.409640Z",
+ "shell.execute_reply": "2026-09-03T10:37:41.408878Z"
},
"papermill": {
"duration": 3.919631,
@@ -988,9 +1674,17 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "[TaskRunFireFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD003 x1\r\n AGENTGUARD003 @ 14:9 Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini\r\n[TaskRunFireCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[TaskRunFireAssignation.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[TaskRunFireDiscard.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[TaskRunFireHomonyme.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "[TaskRunFireFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD003 x1\r\n",
+ " AGENTGUARD003 @ 14:9 Task.Run 'Task.Run(() => Console.WriteLine(\"ping\"))' lance une tache non observee -- exceptions perdues, comportement indefini\r\n",
+ "[TaskRunFireCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[TaskRunFireAssignation.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[TaskRunFireDiscard.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[TaskRunFireHomonyme.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "\r\n"
+ ]
}
],
"source": [
@@ -1065,14 +1759,14 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 14,
"id": "4ccf84dc",
"metadata": {
"execution": {
- "iopub.execute_input": "2026-08-28T21:33:25.415485Z",
- "iopub.status.busy": "2026-08-28T21:33:25.415485Z",
- "iopub.status.idle": "2026-08-28T21:33:25.439909Z",
- "shell.execute_reply": "2026-08-28T21:33:25.439909Z"
+ "iopub.execute_input": "2026-09-03T10:37:41.411761Z",
+ "iopub.status.busy": "2026-09-03T10:37:41.411383Z",
+ "iopub.status.idle": "2026-09-03T10:37:41.452745Z",
+ "shell.execute_reply": "2026-09-03T10:37:41.452454Z"
},
"papermill": {
"duration": 0.03179,
@@ -1085,9 +1779,11 @@
},
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "Exercice a completer : etendre AGENTGUARD003 a Task.Factory.StartNew nu\r\n"
+ "output_type": "stream",
+ "text": [
+ "Exercice a completer : etendre AGENTGUARD003 a Task.Factory.StartNew nu\r\n"
+ ]
}
],
"source": [
@@ -1102,67 +1798,254 @@
{
"cell_type": "markdown",
"id": "d1699700",
- "source": "## D3 — `AGENTGUARD004` : l'annulation doit traverser toute la chaîne\n\nUn agent reçoit souvent un `CancellationToken` depuis une requête HTTP ou un `BackgroundService`. Le token n'est utile que s'il voyage jusqu'à chaque opération annulable. Le défaut est discret : la méthode englobante possède bien le token, la cible l'accepte, mais l'appel omet l'argument optionnel. Le code compile et continue à travailler après l'annulation demandée.\n\n`AGENTGUARD004` vise exactement cette rupture. Il ne cherche ni un nom comme `Async`, ni un paramètre nommé `ct` : il demande au modèle sémantique si le symbole englobant possède un vrai `System.Threading.CancellationToken`, puis si la signature cible expose ce même type et si l'argument a été fourni explicitement.",
- "metadata": {}
+ "metadata": {},
+ "source": [
+ "## D3 — `AGENTGUARD004` : l'annulation doit traverser toute la chaîne\n",
+ "\n",
+ "Un agent reçoit souvent un `CancellationToken` depuis une requête HTTP ou un `BackgroundService`. Le token n'est utile que s'il voyage jusqu'à chaque opération annulable. Le défaut est discret : la méthode englobante possède bien le token, la cible l'accepte, mais l'appel omet l'argument optionnel. Le code compile et continue à travailler après l'annulation demandée.\n",
+ "\n",
+ "`AGENTGUARD004` vise exactement cette rupture. Il ne cherche ni un nom comme `Async`, ni un paramètre nommé `ct` : il demande au modèle sémantique si le symbole englobant possède un vrai `System.Threading.CancellationToken`, puis si la signature cible expose ce même type et si l'argument a été fourni explicitement."
+ ]
},
{
"cell_type": "code",
+ "execution_count": 15,
"id": "24a493a4",
- "source": "// Exemple guide resolu -- lire l'analyseur semantique livre.\nvar analyzer004 = File.ReadAllText(Path.Combine(\n here, \"AgentGuard.Analyzers\", \"CancellationTokenPropagationAnalyzer.cs\"));\nConsole.WriteLine(analyzer004);",
- "metadata": {},
- "execution_count": 14,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-03T10:37:41.455088Z",
+ "iopub.status.busy": "2026-09-03T10:37:41.454653Z",
+ "iopub.status.idle": "2026-09-03T10:37:41.498290Z",
+ "shell.execute_reply": "2026-09-03T10:37:41.497987Z"
+ }
+ },
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "using System.Collections.Immutable;\nusing System.Linq;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.Diagnostics;\nusing Microsoft.CodeAnalysis.Operations;\n\nnamespace AgentGuard.Analyzers;\n\n/// \n/// AGENTGUARD004 : perte d'un CancellationToken disponible.\n///\n/// Une méthode d'agent reçoit souvent un token depuis la requête HTTP ou le\n/// BackgroundService. Si elle appelle une opération dont la signature expose\n/// explicitement CancellationToken mais omet cet argument optionnel, l'arrêt\n/// demandé ne traverse plus la chaîne. L'analyse est entièrement sémantique :\n/// le type doit être System.Threading.CancellationToken, sans heuristique sur\n/// le nom de la méthode ni du paramètre.\n/// \n[DiagnosticAnalyzer(LanguageNames.CSharp)]\npublic sealed class CancellationTokenPropagationAnalyzer : DiagnosticAnalyzer\n{\n public const string DiagnosticId = \"AGENTGUARD004\";\n\n private static readonly DiagnosticDescriptor Rule = new(\n DiagnosticId,\n \"CancellationToken disponible mais non propage\",\n \"L'appel à '{0}' omet CancellationToken alors que '{1}' est disponible dans la méthode englobante\",\n \"Agentisme\",\n DiagnosticSeverity.Warning,\n isEnabledByDefault: true,\n description: \"Propager le CancellationToken disponible aux opérations annulables afin que l'arrêt traverse toute la chaîne d'agent.\");\n\n public override ImmutableArray SupportedDiagnostics\n => ImmutableArray.Create(Rule);\n\n public override void Initialize(AnalysisContext context)\n {\n context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n context.EnableConcurrentExecution();\n context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);\n }\n\n private static void AnalyzeInvocation(OperationAnalysisContext ctx)\n {\n var invocation = (IInvocationOperation)ctx.Operation;\n var cancellationToken = ctx.Compilation.GetTypeByMetadataName(\n \"System.Threading.CancellationToken\");\n if (cancellationToken is null) return;\n\n var tokenParameter = invocation.TargetMethod.Parameters.FirstOrDefault(\n parameter => SymbolEqualityComparer.Default.Equals(\n parameter.Type, cancellationToken));\n if (tokenParameter is null) return;\n\n // Roslyn matérialise un argument optionnel omis avec ArgumentKind.DefaultValue.\n // Seul un argument Explicit prouve que l'appelant a propagé un token.\n var tokenIsExplicit = invocation.Arguments.Any(argument =>\n SymbolEqualityComparer.Default.Equals(argument.Parameter, tokenParameter)\n && argument.ArgumentKind == ArgumentKind.Explicit);\n if (tokenIsExplicit) return;\n\n var availableToken = FindAvailableToken(ctx.ContainingSymbol, cancellationToken);\n if (availableToken is null) return;\n\n ctx.ReportDiagnostic(Diagnostic.Create(\n Rule,\n invocation.Syntax.GetLocation(),\n invocation.TargetMethod.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),\n availableToken.Name));\n }\n\n private static IParameterSymbol FindAvailableToken(\n ISymbol symbol,\n INamedTypeSymbol cancellationToken)\n {\n // Une lambda ou une fonction locale peut capturer le token de sa méthode\n // englobante : on remonte donc la chaîne de symboles, sans sortir du type.\n for (var current = symbol; current is not null && current is not INamedTypeSymbol;\n current = current.ContainingSymbol)\n {\n if (current is not IMethodSymbol method) continue;\n\n var token = method.Parameters.FirstOrDefault(parameter =>\n SymbolEqualityComparer.Default.Equals(\n parameter.Type, cancellationToken));\n if (token is not null) return token;\n }\n\n return null;\n }\n}\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "using System.Collections.Immutable;\n",
+ "using System.Linq;\n",
+ "using Microsoft.CodeAnalysis;\n",
+ "using Microsoft.CodeAnalysis.Diagnostics;\n",
+ "using Microsoft.CodeAnalysis.Operations;\n",
+ "\n",
+ "namespace AgentGuard.Analyzers;\n",
+ "\n",
+ "/// \n",
+ "/// AGENTGUARD004 : perte d'un CancellationToken disponible.\n",
+ "///\n",
+ "/// Une méthode d'agent reçoit souvent un token depuis la requête HTTP ou le\n",
+ "/// BackgroundService. Si elle appelle une opération dont la signature expose\n",
+ "/// explicitement CancellationToken mais omet cet argument optionnel, l'arrêt\n",
+ "/// demandé ne traverse plus la chaîne. L'analyse est entièrement sémantique :\n",
+ "/// le type doit être System.Threading.CancellationToken, sans heuristique sur\n",
+ "/// le nom de la méthode ni du paramètre.\n",
+ "/// \n",
+ "[DiagnosticAnalyzer(LanguageNames.CSharp)]\n",
+ "public sealed class CancellationTokenPropagationAnalyzer : DiagnosticAnalyzer\n",
+ "{\n",
+ " public const string DiagnosticId = \"AGENTGUARD004\";\n",
+ "\n",
+ " private static readonly DiagnosticDescriptor Rule = new(\n",
+ " DiagnosticId,\n",
+ " \"CancellationToken disponible mais non propage\",\n",
+ " \"L'appel à '{0}' omet CancellationToken alors que '{1}' est disponible dans la méthode englobante\",\n",
+ " \"Agentisme\",\n",
+ " DiagnosticSeverity.Warning,\n",
+ " isEnabledByDefault: true,\n",
+ " description: \"Propager le CancellationToken disponible aux opérations annulables afin que l'arrêt traverse toute la chaîne d'agent.\");\n",
+ "\n",
+ " public override ImmutableArray SupportedDiagnostics\n",
+ " => ImmutableArray.Create(Rule);\n",
+ "\n",
+ " public override void Initialize(AnalysisContext context)\n",
+ " {\n",
+ " context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);\n",
+ " context.EnableConcurrentExecution();\n",
+ " context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);\n",
+ " }\n",
+ "\n",
+ " private static void AnalyzeInvocation(OperationAnalysisContext ctx)\n",
+ " {\n",
+ " var invocation = (IInvocationOperation)ctx.Operation;\n",
+ " var cancellationToken = ctx.Compilation.GetTypeByMetadataName(\n",
+ " \"System.Threading.CancellationToken\");\n",
+ " if (cancellationToken is null) return;\n",
+ "\n",
+ " var tokenParameter = invocation.TargetMethod.Parameters.FirstOrDefault(\n",
+ " parameter => SymbolEqualityComparer.Default.Equals(\n",
+ " parameter.Type, cancellationToken));\n",
+ " if (tokenParameter is null) return;\n",
+ "\n",
+ " // Roslyn matérialise un argument optionnel omis avec ArgumentKind.DefaultValue.\n",
+ " // Seul un argument Explicit prouve que l'appelant a propagé un token.\n",
+ " var tokenIsExplicit = invocation.Arguments.Any(argument =>\n",
+ " SymbolEqualityComparer.Default.Equals(argument.Parameter, tokenParameter)\n",
+ " && argument.ArgumentKind == ArgumentKind.Explicit);\n",
+ " if (tokenIsExplicit) return;\n",
+ "\n",
+ " var availableToken = FindAvailableToken(ctx.ContainingSymbol, cancellationToken);\n",
+ " if (availableToken is null) return;\n",
+ "\n",
+ " ctx.ReportDiagnostic(Diagnostic.Create(\n",
+ " Rule,\n",
+ " invocation.Syntax.GetLocation(),\n",
+ " invocation.TargetMethod.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),\n",
+ " availableToken.Name));\n",
+ " }\n",
+ "\n",
+ " private static IParameterSymbol FindAvailableToken(\n",
+ " ISymbol symbol,\n",
+ " INamedTypeSymbol cancellationToken)\n",
+ " {\n",
+ " // Une lambda ou une fonction locale peut capturer le token de sa méthode\n",
+ " // englobante : on remonte donc la chaîne de symboles, sans sortir du type.\n",
+ " for (var current = symbol; current is not null && current is not INamedTypeSymbol;\n",
+ " current = current.ContainingSymbol)\n",
+ " {\n",
+ " if (current is not IMethodSymbol method) continue;\n",
+ "\n",
+ " var token = method.Parameters.FirstOrDefault(parameter =>\n",
+ " SymbolEqualityComparer.Default.Equals(\n",
+ " parameter.Type, cancellationToken));\n",
+ " if (token is not null) return token;\n",
+ " }\n",
+ "\n",
+ " return null;\n",
+ " }\n",
+ "}\n",
+ "\r\n"
+ ]
}
+ ],
+ "source": [
+ "// Exemple guide resolu -- lire l'analyseur semantique livre.\n",
+ "var analyzer004 = File.ReadAllText(Path.Combine(\n",
+ " here, \"AgentGuard.Analyzers\", \"CancellationTokenPropagationAnalyzer.cs\"));\n",
+ "Console.WriteLine(analyzer004);"
]
},
{
"cell_type": "markdown",
"id": "0d9de673",
- "source": "### Mesure — un fautif, quatre contrôles négatifs\n\nLe Verifier soumet cinq sources au même analyseur :\n\n1. token disponible + cible annulable + argument omis : **rouge** ;\n2. token transmis : propre ;\n3. aucun token disponible : propre ;\n4. surcharge réellement choisie sans paramètre token : propre ;\n5. type homonyme `AgentCustom.CancellationToken` : propre.\n\nCe dernier contrôle est décisif : le filtre compare les symboles au type BCL `System.Threading.CancellationToken`, pas leur simple orthographe.",
- "metadata": {}
+ "metadata": {},
+ "source": [
+ "### Mesure — un fautif, quatre contrôles négatifs\n",
+ "\n",
+ "Le Verifier soumet cinq sources au même analyseur :\n",
+ "\n",
+ "1. token disponible + cible annulable + argument omis : **rouge** ;\n",
+ "2. token transmis : propre ;\n",
+ "3. aucun token disponible : propre ;\n",
+ "4. surcharge réellement choisie sans paramètre token : propre ;\n",
+ "5. type homonyme `AgentCustom.CancellationToken` : propre.\n",
+ "\n",
+ "Ce dernier contrôle est décisif : le filtre compare les symboles au type BCL `System.Threading.CancellationToken`, pas leur simple orthographe."
+ ]
},
{
"cell_type": "code",
+ "execution_count": 16,
"id": "87b32afa",
- "source": "var verdicts004 = Shell.Run(here, \"dotnet\",\n \"run --project AgentGuard.Verifier -- \" +\n \"AgentGuard.Verifier/samples/CancellationTokenFautif.cs \" +\n \"AgentGuard.Verifier/samples/CancellationTokenCorrige.cs \" +\n \"AgentGuard.Verifier/samples/CancellationTokenSansTokenDisponible.cs \" +\n \"AgentGuard.Verifier/samples/CancellationTokenSurchargeSansToken.cs \" +\n \"AgentGuard.Verifier/samples/CancellationTokenHomonyme.cs\");\nConsole.WriteLine(verdicts004);",
- "metadata": {},
- "execution_count": 15,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-03T10:37:41.500757Z",
+ "iopub.status.busy": "2026-09-03T10:37:41.500247Z",
+ "iopub.status.idle": "2026-09-03T10:37:48.080032Z",
+ "shell.execute_reply": "2026-09-03T10:37:48.079582Z"
+ }
+ },
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "[CancellationTokenFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD004 x1\r\n AGENTGUARD004 @ 10:15 L'appel à 'Task AgentCancellationFautif.EnvoyerAuModeleAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante\r\n[CancellationTokenCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[CancellationTokenSansTokenDisponible.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[CancellationTokenSurchargeSansToken.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n[CancellationTokenHomonyme.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n\r\n"
+ "output_type": "stream",
+ "text": [
+ "[CancellationTokenFautif.cs] VERDICT : 1 diagnostic(s) -- AGENTGUARD004 x1\r\n",
+ " AGENTGUARD004 @ 10:15 L'appel à 'Task AgentCancellationFautif.EnvoyerAuModeleAsync(string prompt, CancellationToken cancellationToken = default(CancellationToken))' omet CancellationToken alors que 'cancellationToken' est disponible dans la méthode englobante\r\n",
+ "[CancellationTokenCorrige.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[CancellationTokenSansTokenDisponible.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[CancellationTokenSurchargeSansToken.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "[CancellationTokenHomonyme.cs] VERDICT : PROPRE -- aucun garde-fou AgentGuard declenche\r\n",
+ "\r\n"
+ ]
}
+ ],
+ "source": [
+ "var verdicts004 = Shell.Run(here, \"dotnet\",\n",
+ " \"run --project AgentGuard.Verifier -- \" +\n",
+ " \"AgentGuard.Verifier/samples/CancellationTokenFautif.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/CancellationTokenCorrige.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/CancellationTokenSansTokenDisponible.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/CancellationTokenSurchargeSansToken.cs \" +\n",
+ " \"AgentGuard.Verifier/samples/CancellationTokenHomonyme.cs\");\n",
+ "Console.WriteLine(verdicts004);"
]
},
{
"cell_type": "markdown",
"id": "8e4272a6",
- "source": "### Lecture du résultat\n\nLe terrain fautif rend exactement **`AGENTGUARD004 x1`** sur l'appel qui perd l'annulation. Les quatre autres terrains rendent **PROPRE** : le diagnostic disparaît dès que le vrai token est transmis, et il n'apparaît pas quand le contrat ou la portée ne permettent aucune propagation.\n\nLa distinction « surcharge sans token » évite une recommandation impossible : si la méthode réellement résolue par Roslyn n'expose pas `CancellationToken`, l'analyseur ne prétend pas qu'un argument pourrait lui être ajouté. La distinction « homonyme » évite le faux positif lexical. Ces résultats mesurés bornent précisément ce que garantit la règle : **propager un token déjà disponible vers une cible qui l'accepte explicitement**.",
- "metadata": {}
+ "metadata": {},
+ "source": [
+ "### Lecture du résultat\n",
+ "\n",
+ "Le terrain fautif rend exactement **`AGENTGUARD004 x1`** sur l'appel qui perd l'annulation. Les quatre autres terrains rendent **PROPRE** : le diagnostic disparaît dès que le vrai token est transmis, et il n'apparaît pas quand le contrat ou la portée ne permettent aucune propagation.\n",
+ "\n",
+ "La distinction « surcharge sans token » évite une recommandation impossible : si la méthode réellement résolue par Roslyn n'expose pas `CancellationToken`, l'analyseur ne prétend pas qu'un argument pourrait lui être ajouté. La distinction « homonyme » évite le faux positif lexical. Ces résultats mesurés bornent précisément ce que garantit la règle : **propager un token déjà disponible vers une cible qui l'accepte explicitement**."
+ ]
},
{
"cell_type": "markdown",
"id": "ef312734",
- "source": "### Exercice 5 — Borner la portée dans une fonction locale\n\nL'analyseur remonte les symboles englobants : une fonction locale ou une lambda peut capturer le token de sa méthode parente. Cette propriété évite de perdre l'annulation dans un callback interne, mais elle doit rester précise.\n\n**Objectif** : écrire un terrain qui prouve la capture, puis un contre-exemple où un token local est déjà transmis.\n\n- `# Etape 1` : dans une méthode `TraiterAsync(CancellationToken ct)`, déclarer une fonction locale `EtapeAsync()` qui appelle une cible annulable sans `ct` ; verdict attendu : `AGENTGUARD004`.\n- `# Etape 2` : transmettre `ct` depuis la fonction locale ; verdict attendu : PROPRE.\n- `# Indice` : ne modifiez pas les noms des méthodes pour aider l'analyseur — il ne les utilise pas.\n- `# TODO etudiant` : ajouter les deux sources aux samples du Verifier et expliquer quel `ContainingSymbol` rend la capture visible.",
- "metadata": {}
+ "metadata": {},
+ "source": [
+ "### Exercice 5 — Borner la portée dans une fonction locale\n",
+ "\n",
+ "L'analyseur remonte les symboles englobants : une fonction locale ou une lambda peut capturer le token de sa méthode parente. Cette propriété évite de perdre l'annulation dans un callback interne, mais elle doit rester précise.\n",
+ "\n",
+ "**Objectif** : écrire un terrain qui prouve la capture, puis un contre-exemple où un token local est déjà transmis.\n",
+ "\n",
+ "- `# Etape 1` : dans une méthode `TraiterAsync(CancellationToken ct)`, déclarer une fonction locale `EtapeAsync()` qui appelle une cible annulable sans `ct` ; verdict attendu : `AGENTGUARD004`.\n",
+ "- `# Etape 2` : transmettre `ct` depuis la fonction locale ; verdict attendu : PROPRE.\n",
+ "- `# Indice` : ne modifiez pas les noms des méthodes pour aider l'analyseur — il ne les utilise pas.\n",
+ "- `# TODO etudiant` : ajouter les deux sources aux samples du Verifier et expliquer quel `ContainingSymbol` rend la capture visible."
+ ]
},
{
"cell_type": "code",
+ "execution_count": 17,
"id": "905bc07f",
- "source": "// Exercice 5 a completer -- terrain minimal de fonction locale.\n// TODO etudiant : transformer ce commentaire en deux samples du Verifier.\n//\n// static async Task TraiterAsync(CancellationToken ct)\n// {\n// async Task EtapeAsync()\n// {\n// await OperationAnnulableAsync(); // attendu : AGENTGUARD004\n// // puis corriger avec OperationAnnulableAsync(ct)\n// }\n// await EtapeAsync();\n// }\nConsole.WriteLine(\"Exercice a completer : verifier la propagation dans une fonction locale\");",
- "metadata": {},
- "execution_count": 16,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-03T10:37:48.082230Z",
+ "iopub.status.busy": "2026-09-03T10:37:48.081929Z",
+ "iopub.status.idle": "2026-09-03T10:37:48.118269Z",
+ "shell.execute_reply": "2026-09-03T10:37:48.117968Z"
+ }
+ },
"outputs": [
{
- "output_type": "stream",
"name": "stdout",
- "text": "Exercice a completer : verifier la propagation dans une fonction locale\r\n"
+ "output_type": "stream",
+ "text": [
+ "Exercice a completer : verifier la propagation dans une fonction locale\r\n"
+ ]
}
+ ],
+ "source": [
+ "// Exercice 5 a completer -- terrain minimal de fonction locale.\n",
+ "// TODO etudiant : transformer ce commentaire en deux samples du Verifier.\n",
+ "//\n",
+ "// static async Task TraiterAsync(CancellationToken ct)\n",
+ "// {\n",
+ "// async Task EtapeAsync()\n",
+ "// {\n",
+ "// await OperationAnnulableAsync(); // attendu : AGENTGUARD004\n",
+ "// // puis corriger avec OperationAnnulableAsync(ct)\n",
+ "// }\n",
+ "// await EtapeAsync();\n",
+ "// }\n",
+ "Console.WriteLine(\"Exercice a completer : verifier la propagation dans une fonction locale\");"
]
}
],
@@ -1182,4 +2065,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
\ No newline at end of file
+}