Docker heeft zijn agent-runtime een nieuwe naam en een eigen plek in de CLI gegeven. Docker Agent stond deze week op de voorpagina van Hacker News met ruim 200 punten en een kleine 100 reacties. Een groot deel daarvan was sceptisch. "Agent harnesses are turning into JS frameworks from yesteryear", schreef iemand. Iedereen heeft er tegenwoordig een.
Die scepsis snap ik. Toch vind ik het idee eronder interessant voor wie een .NET-codebase onderhoudt. Een agent wordt een bestand in je repo, met een diff en een versienummer. En de draad in de discussie die er echt toe deed ging over iets heel anders dan Docker: agents die zeggen dat ze klaar zijn terwijl dat niet zo is.
Wat Docker Agent eigenlijk is
Docker Agent is een CLI-plugin (docker agent) waarmee je agents declaratief beschrijft in YAML. Je geeft een agent een model, een instructie en een set tools, en de runtime doet de rest. Volgens een Docker-medewerker in de HN-thread begon het project bijna twee jaar geleden als "the docker compose for agents", onder de naam cagent. Die erfenis zie je nog terug: de globale config staat in ~/.config/cagent/config.yaml.
Wat er in de doos zit, volgens de documentatie:
- modellen van OpenAI, Anthropic, Gemini, AWS Bedrock, Mistral, xAI en lokaal via Docker Model Runner
- ingebouwde toolsets zoals
filesystem,shell,think,todoenmemory, plus elke MCP-server - multi-agent via
sub_agents, waarbij een root-agent werk doorzet naar specialisten docker agent share pushenpullom agents als OCI-artifact via een registry te verspreiden- een headless modus (
docker agent run --exec) en serve-commando's voor een HTTP-API, MCP en A2A
Het is Apache 2.0. Dat zegt niks over de kwaliteit, maar wel dat je niet vastzit.
Een agent als bestand in je repo
Het sterkste argument voor dit soort tools is saai. Een agentdefinitie die in YAML naast je .sln staat, kun je reviewen. Je ziet in een pull request dat iemand de shell-toolset aan de reviewer-agent heeft gegeven. Je ziet dat het model is gewisseld. In een chatvenster dat elke developer anders heeft ingesteld zie je dat nooit.
Zo ziet een opzet eruit die ik voor een .NET-solution zou gebruiken:
agents:
root:
model: anthropic/claude-sonnet-4-5
description: Coördinator voor wijzigingen in de .NET-solution
instruction: |
Verdeel het werk tussen coder en reviewer.
Een taak is pas klaar als run_tests GROEN teruggeeft
en de reviewer geen blokkerende punten meer heeft.
sub_agents: [coder, reviewer]
coder:
model: anthropic/claude-sonnet-4-5
description: Past C#-code en tests aan
instruction: |
Je werkt in een .NET 10-solution met xUnit.
Draai run_tests na elke wijziging.
Plak bij "klaar" de letterlijke uitvoer van run_tests.
toolsets:
- type: filesystem
- type: mcp
command: dotnet
args: ["tools/AgentTools/bin/Release/net10.0/AgentTools.dll"]
tools: ["run_tests"]
reviewer:
model: anthropic/claude-sonnet-4-5
description: Leest wijzigingen en zoekt bugs
instruction: Review de wijzigingen op bugs en ontbrekende tests. Je schrijft zelf geen code.
toolsets:
- type: filesystem
tools: ["read_file", "search_files_content"]
permissions:
deny:
- "shell:cmd=git push*"
- "shell:cmd=rm*-rf*"
- "shell:cmd=sudo*"
Een paar keuzes hierin zijn bewust. De reviewer krijgt via de tools-filter alleen leestools. De docs zeggen zelf dat minder tools het model minder verwart, en een reviewer die bestanden kan schrijven wordt vroeg of laat een tweede coder. De coder heeft geen generieke shell. Hij krijgt één tool om tests te draaien, en die tool is van ons.
Je eigen .NET-tooling als MCP-server
Een generieke shell geven en hopen dat de agent dotnet test correct aanroept, werkt prima tot de uitvoer 3000 regels lang is en het model halverwege de draad kwijtraakt. In de HN-thread meldde iemand precies dat: sessies die vastliepen zodra een antwoord boven de 10.000 tekens kwam. Lange testlogs zijn daar een prima manier voor.
Dus bouw je een kleine MCP-server die het vuile werk doet en een kort, eenduidig antwoord teruggeeft. Met de officiële C# SDK (NuGet-pakketten ModelContextProtocol en Microsoft.Extensions.Hosting) is dat een consoleproject met dit erin:
using System.ComponentModel;
using System.Diagnostics;
using System.Xml.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
// stdout is het MCP-kanaal. Alle logging moet naar stderr.
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
[McpServerToolType]
public static class DotnetTools
{
private static readonly XNamespace Trx =
"http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
[McpServerTool(Name = "run_tests")]
[Description("Bouwt de solution en draait de tests. Geeft GROEN of ROOD terug, met de falende tests.")]
public static async Task<string> RunTests(
[Description("Optionele filterexpressie voor dotnet test --filter")] string? filter,
CancellationToken ct)
{
var resultsDir = Path.Combine(Path.GetTempPath(), "agent-tests", Guid.NewGuid().ToString("N"));
var psi = new ProcessStartInfo("dotnet")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (var arg in new[] { "test", "--nologo", "--logger", "trx", "--results-directory", resultsDir })
psi.ArgumentList.Add(arg);
if (!string.IsNullOrWhiteSpace(filter))
{
psi.ArgumentList.Add("--filter");
psi.ArgumentList.Add(filter);
}
using var proc = Process.Start(psi)!;
// Beide streams leeglezen, anders blokkeert het proces op een volle buffer.
var stdout = proc.StandardOutput.ReadToEndAsync(ct);
var stderr = proc.StandardError.ReadToEndAsync(ct);
await proc.WaitForExitAsync(ct);
await Task.WhenAll(stdout, stderr);
List<string> trxFiles = Directory.Exists(resultsDir)
? [.. Directory.EnumerateFiles(resultsDir, "*.trx", SearchOption.AllDirectories)]
: [];
if (trxFiles.Count == 0)
return $"ROOD: build of testrun mislukt (exit {proc.ExitCode})\n{Tail(await stdout, 40)}";
int total = 0, passed = 0;
var failures = new List<string>();
foreach (var file in trxFiles)
{
var doc = XDocument.Load(file);
var counters = doc.Descendants(Trx + "Counters").First();
total += (int)counters.Attribute("total")!;
passed += (int)counters.Attribute("passed")!;
failures.AddRange(doc.Descendants(Trx + "UnitTestResult")
.Where(r => (string?)r.Attribute("outcome") == "Failed")
.Select(r => $"- {(string?)r.Attribute("testName")}: " +
Truncate((string?)r.Descendants(Trx + "Message").FirstOrDefault(), 300)));
}
Directory.Delete(resultsDir, recursive: true);
var status = proc.ExitCode == 0 && failures.Count == 0 ? "GROEN" : "ROOD";
return $"{status}: {passed}/{total} geslaagd (exit {proc.ExitCode})\n" +
string.Join('\n', failures.Take(20));
}
private static string Tail(string s, int lines) =>
string.Join('\n', s.Split('\n').TakeLast(lines));
private static string Truncate(string? s, int max) =>
s is null ? "" : s.Length <= max ? s : s[..max] + "...";
}
Twee details die je pas tegenkomt als het misgaat. Het eerste: stdout is bij een stdio-MCP-server het protocolkanaal. Eén Console.WriteLine of een logregel op de verkeerde stream en de client krijgt kapotte JSON-RPC binnen. Daarom gaat logging expliciet naar stderr. Het tweede: start de server niet met dotnet run. Dan loopt de build eerst, en alles wat MSBuild op stdout zet komt ook in het protocolkanaal terecht. Bouw het toolproject vooraf met dotnet build -c Release en wijs in de YAML naar de dll, zoals hierboven.
Het antwoord van de tool is met opzet kort. GROEN of ROOD, een telling en hooguit twintig falende tests met een ingekorte foutmelding. Het model hoeft niets te interpreteren. Bonus: dezelfde server werkt ook in Claude Code, Copilot of elke andere MCP-client. Je zit dus niet vast aan Docker.
De agent die zegt dat hij klaar is
In de HN-reacties ging het nauwelijks over YAML. Meerdere developers wezen op twee andere problemen: agents die over langere tijd de samenhang kwijtraken, en agents die claimen dat een taak af is terwijl dat niet zo is. Wie een paar weken met coding agents werkt, herkent het: "Alle tests slagen!", terwijl de agent de falende test heeft overgeslagen of met [Fact(Skip = "...")] heeft uitgezet.
Een instructie als "meld pas klaar als run_tests GROEN is" helpt. Maar het blijft een verzoek aan het model. De verificatie moet buiten de agent gebeuren, in een stap die het model niet kan beïnvloeden. Headless ziet dat er zo uit:
#!/usr/bin/env bash
set -euo pipefail
# Draait op een wegwerp-runner met een verse checkout.
# --yolo omdat niemand meekijkt; de deny-regels uit agent.yaml gelden nog steeds.
docker agent run --exec --yolo --json agent.yaml \
"Los de falende test in OrderServiceTests op zonder tests over te slaan" \
> agent-session.ndjson
# Wat de agent beweert interesseert ons hier niet. Dit wel:
dotnet build -c Release -warnaserror
dotnet test -c Release --no-build
# Nieuwe Skip-attributen zijn een harde fout, ook in nieuwe bestanden.
git add --intent-to-add .
if git diff --unified=0 HEAD -- '*.cs' | grep -E '^\+.*Skip\s*=' ; then
echo "Agent heeft tests uitgeschakeld" >&2
exit 1
fi
Over --yolo hoef je je weinig zorgen te maken zolang de deny-lijst klopt. De permissions-docs beschrijven de volgorde als deny, dan allow, dan ask. In autonome modus draait alles behalve wat je expliciet blokkeert. In CI is de wegwerp-runner je isolatie. Lokaal combineer je het met --sandbox of --worktree, zodat de agent je eigen checkout niet aanraakt.
De grep op Skip is primitief en dat is prima. Het gaat om het principe: elke manier waarop een agent "klaar" kan faken, wil je mechanisch kunnen detecteren. Andere kandidaten die ik in .NET-repo's tegenkom: #pragma warning disable die ineens opduikt, een <NoWarn> in een .csproj, of <Nullable>disable</Nullable> op projectniveau.
Deny eerst, de rest daarna
De permissions verdienen meer aandacht dan ze in de meeste demo's krijgen. Standaard keurt Docker Agent alleen read-only tools automatisch goed en vraagt het bij de rest om bevestiging. Interactief werkt dat. Bij twintig vragen per sessie klik je echter alles weg zonder te lezen, en dan kun je net zo goed --yolo zetten.
Mijn advies: een korte, harde deny-lijst die je nooit wilt laten passeren, plus een allow-lijst voor het saaie werk dat je vertrouwt. Patronen ondersteunen argumentmatching, zodat je shell:cmd=git push* apart kunt blokkeren zonder git status te verbieden. Denk ook aan schrijfrechten op .github/workflows. Een agent die zijn eigen pipeline mag aanpassen, kan ook je verificatiestap aanpassen. Maak voor dat pad een ask- of deny-regel, of laat het via een CODEOWNERS-regel altijd door een mens reviewen.
Agents uit een registry trekken
Agents verspreiden via een OCI-registry klinkt handig. docker agent run myorg/agent:tag en je hele team draait dezelfde configuratie. Maar haal je zo een agent van een ander binnen, dan haal je ook zijn instructies, zijn MCP-servers en zijn rechten binnen. Dat is supply chain, net zo goed als een NuGet-pakket of een base image.
Behandel het dan ook zo. docker agent share push en pull ondersteunen ondertekenen en verifiëren met --key. Gebruik dat. Trek een agent uit een publieke registry niet blind in je CI. Pull hem, lees de YAML, en push een gecontroleerde kopie naar je eigen registry. Kijk vooral naar MCP-toolsets met een command: dat is code die op jouw machine draait met jouw rechten.
Wanneer ik het zou gebruiken, en wanneer niet
Gebruik ik Docker Agent voor mijn dagelijkse werk in de editor? Waarschijnlijk niet. Daar heb ik al een agent en die doet wat hij moet doen. De waarde zit in gedeelde, herhaalbare taken: een dependency-update-agent die elke maandag draait, een migratieagent die een bibliotheek over vijftig services uitrolt, een reviewer die voor elke PR dezelfde checks doet. Dan wil je dat de definitie in git staat en dat het model gepind is.
De kritiek in de thread neem ik wel serieus. Mensen meldden broosheid in vroege versies, en een commenter klaagde dat de tool modeluitvoer op eigen houtje parseert zonder een standaard als ACP. En de markt voor agent-runtimes verschuift elke maand. Dat is een reden om je investering klein te houden. Het YAML-bestand is goedkoop weg te gooien. De MCP-server met je eigen tools en het verificatiescript zijn het deel dat blijft, ongeacht welke runtime er volgend jaar bovenaan Hacker News staat.
Veelgestelde vragen over Docker Agent en .NET
Heb je Docker Desktop nodig om Docker Agent te gebruiken? Nee, Docker Agent is een CLI-plugin en een open-source project onder Apache 2.0 dat je ook los kunt installeren. Docker Desktop maakt het wel makkelijker om lokale modellen via Docker Model Runner te draaien en MCP-servers uit de Docker-catalogus te gebruiken.
Kan ik een MCP-server in C# ook met andere agents gebruiken? Ja, een stdio-MCP-server die je met de ModelContextProtocol-SDK bouwt is niet aan Docker gebonden. Dezelfde dll werkt in elke client die MCP spreekt, zoals Claude Code, GitHub Copilot of een eigen host, zolang je in die client het commando en de argumenten configureert.
Waarom niet gewoon de shell-toolset geven en dotnet test laten draaien? Omdat ruwe testuitvoer lang en rommelig is, en het model daar zelf een conclusie uit moet trekken. Een eigen tool met een kort GROEN of ROOD antwoord kost minder context, is moeilijker verkeerd te lezen en laat zich met de tools-filter precies afbakenen.
Is --yolo verantwoord in een CI-pipeline? Alleen als de agent in een geïsoleerde worktree of sandbox draait, de deny-lijst destructieve commando's en pushes blokkeert, en een stap buiten de agent build en tests opnieuw uitvoert. Zonder die externe verificatie vertrouw je op de eigen claim van het model, en dat is precies wat in de praktijk misgaat.
Verder lezen
- Geef het model geen rijen: MCP-tools ontwerpen voor je eigen productdata
- AI-agent leest je .env: waarom .codexignore telt
- 1,7 GB verstopte binaries in Codex: weet jij wat er in je .NET-artifact zit?
Conclusie: Zet je agentdefinitie gerust in YAML in je repo, maar steek je energie in de twee dingen die elke runtime overleven: een eigen MCP-server die korte, eenduidige antwoorden geeft, en een verificatiestap buiten de agent die build, tests en uitgeschakelde checks zelf controleert. Wat de agent over zijn eigen werk zegt, is input. Wat dotnet test zegt, is de uitslag.
Bronnen: Docker Agent op GitHub · Docker Agent-documentatie · Hacker News-discussie · Model Context Protocol C# SDK.
