Skip to main content

AgenticDockerTester: giving an LLM NET_ADMIN inside a container and telling it to be creative

Share:XLinkedInHN
Cover for AgenticDockerTester: giving an LLM NET_ADMIN inside a container and telling it to be creative

What this repo actually is

kaushiksaravanan/AgenticDockerTest is a Python 3.10+ CLI called agentictester. It watches a Git repo, finds source files that carry a specific header block, and hands each one to an LLM agent running inside a fresh Docker container. The agent is told to be creative about breaking things: fake low storage with dd, tear down network reachability with iptables, put memory pressure on the process with stress-ng and cgroups, and see what happens.

I want to be upfront about the state of this one. It is a single 30k-line commit (221bdfd) with one follow-up README tweak, last pushed 2026-02-22. setup.py self-classifies as Development Status :: 3 - Alpha. The author_email field is empty and the GitHub URL is the literal placeholder yourusername/AgenticDockerTester. I do not have evidence I ran it end-to-end against a real target. No CI, no tests/, no demo run captured. What follows is a tour of the design, not a claim that it works in the wild.

The $$$$$$$$ header convention

Rather than pointing the tool at a directory of test files, the design flips the question and asks every source file to opt in. Each file that wants to be tested declares itself with a header block bracketed by long runs of dollar signs. Inside that block are directives:

$$$$$$$$$$$$$$$$$$$$$$$$$
RUN: YES
DESCRIPTION: Idempotent upload path
CONDITIONS:
  network: limited
  storage: constrained
  memory: limited
  permissions: read-only
PRIORITY: high
TAGS: upload, retry
TIMEOUT: 300
RETRY: 2
$$$$$$$$$$$$$$$$$$$$$$$$$

RUN: YES is the switch. Without it the file is skipped. The parser also reads .js, .ts, .java, .go, and .rs, so the file-discovery surface is polyglot even if the execution surface is not (more on that gap below). The interesting move is that the source file, not an external YAML, decides what environment it wants to be tested under. CONDITIONS: network=limited is a hint the TestAgent reads and translates into a set of chaos actions to perform inside the container.

Three agents, three jobs

The orchestrator (700 lines in orchestrator.py) runs three agents per test session.

TestAgent is the interesting one. Its system prompt tells it it has full creative control over the container environment. It picks tools from a small catalog (dd, iptables, tc, stress-ng, chmod, cgroup writes) and decides, per file, how to simulate the conditions declared in the header. If the header says storage: constrained, it might fill /tmp with dd if=/dev/zero of=/tmp/pad bs=1M count=1800 before running the target. If it says network: limited, it might drop outbound packets with iptables -A OUTPUT -j DROP. The point of the LLM is not to run the test, it is to be inventive about the conditions under which the test runs.

ProfilerAgent sits alongside and captures psutil, top, and iostat samples throughout the run. It does not make decisions; it just collects.

SolverAgent (561 lines in solver_agent.py) reads the failure and produces diagnostic suggestions. The design constraint here is the one I care most about: SolverAgent does not auto-fix. It writes DiagnosticSuggestion records into the AnalysisReport and stops. Any code change is a human decision made after reading the report. Given that TestAgent already has NET_ADMIN inside a container, keeping Solver on a suggestion-only ceiling felt like the right line to draw.

Reports render as HTML or PDF via Jinja and ReportLab, and can go out over SMTP if configured.

NET_ADMIN and SYS_ADMIN, and why

The container provisioner (provisioner.py, 616 lines) creates each container with two extra Linux capabilities: NET_ADMIN and SYS_ADMIN. It also drops MKNOD and sets no-new-privileges.

NET_ADMIN is what lets iptables rewrite rules inside the container. Without it, TestAgent could not simulate a network outage from inside the process's own view. SYS_ADMIN is broader than I would like, and it is there because writing cgroup limits from inside the container (memory ceilings, CPU shares) needs it, and because some stress-ng modes touch surfaces that non-privileged containers cannot reach.

The security trade is real. NET_ADMIN + SYS_ADMIN inside a container driven by an LLM is not a shape I would ship as anything other than an experiment on a dev laptop. no-new-privileges and the MKNOD drop cap some of the sharp edges, but you still would not point this at anything you cared about. It is why the repo stays in alpha.

Six LLM providers, with SAP GenAI Hub first-class

llm/providers.py (544 lines) implements a common interface across six providers: SAP GenAI Hub, OpenAI, Gemini, Anthropic, Azure OpenAI, and Ollama. Config selects one per session via LLMConfig.

SAP GenAI Hub is the tell for where this came from. The config manager reads AICORE_AUTH_URL, AICORE_CLIENT_ID, and a resource_group field, which are the exact bindings SAP AI Core uses for OAuth. That is my day-job context leaking into a personal experiment. Ollama being in the list is the other end of the same spectrum: everything can run locally against a small model on a laptop, which is where the creative-chaos idea is safest to try out.

The gap between the header parser and the environment matrix

Here is the thing that keeps this project honest about its alpha status. The header parser scans .py, .js, .ts, .java, .go, and .rs. The default environment matrix, in config/default_config.yaml, ships five Python images:

  • python:3.12-bookworm
  • python:3.10-bullseye
  • python:3.11-alpine3.18
  • python:3.8-slim-buster
  • a custom 3.12-bookworm-full with numpy, pandas, and requests preinstalled

All Python. 2 GB RAM and 1 CPU per container.

So the intake is polyglot, but the execution is Python-only. A .go file with a valid header would be discovered, matched, queued, and then have nowhere to run. The follow-through is a matter of adding Go and Node images to the matrix, but that follow-through has not happened. This is the shape of a project I designed at the highest level of ambition and then implemented only the first slice of.

Flow

flowchart LR
  A[Git repo watcher] --> B{Header parser<br/>scans .py .js .ts .java .go .rs}
  B -->|RUN: YES| C[Orchestrator]
  C --> D[Provisioner<br/>Docker container<br/>NET_ADMIN + SYS_ADMIN]
  D --> E[TestAgent<br/>simulates conditions<br/>via dd, iptables, stress-ng]
  D --> F[ProfilerAgent<br/>psutil, top, iostat]
  E --> G[SolverAgent<br/>suggestions only]
  F --> G
  G --> H[AnalysisReport<br/>HTML / PDF / SMTP]

The React frontend I did not wire up

There is a frontend/ directory in the repo containing a Create-React-App setup wizard with App.js and seven components. It looks like a config builder for the YAML the CLI reads. I do not have evidence it actually talks to the CLI backend. The wizard-generates-yaml shape is a plausible design, but as of the last commit the two halves live next to each other without a documented bridge.

What the alpha state says

One commit of thirty thousand lines with a blank author email and a placeholder GitHub URL is a specific fingerprint. It is the shape of a project that was scaffolded fast, probably with significant LLM assistance in the initial drop, and then set down. The MIT classifier is in setup.py but there is no LICENSE file to back it up.

I do not regret shipping it in that shape. The interesting ideas are the design choices, and the design choices survive the fact that the code has not been battle-tested:

  • Source files declaring their own test conditions, in-file, with a scannable header.
  • A creative LLM agent given real Linux capabilities inside a disposable container, told to invent chaos, not to fix code.
  • A solver that is deliberately capped at suggestion-only, so the loop always ends at a human.
  • Six providers behind one config, with a first-class path for the internal LLM platform I use at work and a local Ollama fallback for the days I do not want to burn tokens.

If I come back to this, the first two moves are obvious: add non-Python images to the matrix so the polyglot parser stops overshooting the environment, and write a single end-to-end run against a small demo repo that I check in alongside the code. Until then, it is a design sketch that happens to compile.

Cite as: Saravanan, K. (2026). AgenticDockerTester: giving an LLM NET_ADMIN inside a container and telling it to be creative. Kaushik Saravanan. https://www.kaushik.cv/blog/agentic-docker-tester