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", + " \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 +}