kubemend series — part 1 of 3
7 min read

I built a Kubernetes remediation agent that can only open pull requests — here's what the eval runs cost and taught me


There’s a genre of “AI SRE” demo where a model diagnoses a Kubernetes incident, and the demo ends there. You’re supposed to be impressed that it found the right answer. Nobody checks whether it would have found the wrong one just as confidently, on a different run, with a slightly different log line.

I wanted to build the version of this that doesn’t ask you to trust the demo. kubemend diagnoses an incident from Prometheus metrics and Loki logs the same way those demos do — but it can’t act on its own conclusion. Its only actuator is opening a draft pull request against a GitOps repo. And the run doesn’t succeed because the model says “fixed” — it succeeds because a verification pipeline the model has no control over re-renders the proposed change, checks it against the same admission policy the cluster would enforce, diffs it against live state, and checks the diff stays inside the declared blast radius. If any of that fails, the model gets the specific failure back and tries again. If it can’t converge, the run ends in a written handoff, not a plausible-looking PR.

The interesting part of building this wasn’t the agent loop. It was how many ways “the harness verified it” turned out to not mean “it actually works” — and how an eval suite that runs the same scenario N times, against a real cluster, is the only thing that reliably surfaces the gap.

🎯 The rule the whole project rests on

Success is decided only by an independent re-run of the validation pipeline. A model’s self-report never ends a run.

Concretely: the agent has a validate_change tool it can call mid-investigation as a cheap self-check. When it stops calling tools and declares the incident fixed, the harness throws that self-check result away and re-runs the entire pipeline itself — helm render, Kyverno policy check, live diff against the cluster, scope check, and (added later, see below) a live quota check. There is no code path where a value the model returned can satisfy that re-run. I have a test that constructs a tool which lies to the model — “everything passed!” — and asserts the harness still returns the true, failing verdict. If that test ever passes for the wrong reason, the whole project’s claim is gone.

This sounds obvious in the abstract. It stopped being obvious the moment I started running the same scenario five times in a row against a real cluster, because that’s when the pipeline’s own bugs started showing up — and every one of them was a case where the harness would have told the model (and a human reviewer) “verified” when the fix didn’t actually work.

🐛 The bug that would have shipped a policy violation

Kyverno — the admission-policy engine the validator runs against every proposed change — reported this on an early run:

pass: 0, fail: 0, warn: 0, error: 0, skip: 0

Zero rules evaluated. My check only looked at whether the command exited successfully, and an exit code of 0 with nothing evaluated looks identical, on the surface, to an exit code of 0 with six policies cleanly passing. The actual cause was a namespace-selector mismatch in how the manifests were rendered — the policies were scoped to a namespace the rendered output didn’t carry, so Kyverno had nothing to check against and shrugged.

A policy check that silently checks nothing is worse than no policy check, because it looks exactly like a passing one. The fix was to fail closed: if the rule-evaluation count is zero, that’s a harness fault, and the verdict is False, not True. I only found this because I was running the scenario against a real cluster repeatedly, not because I reasoned my way to it — the bug was invisible in a code review of the Kyverno-invocation code, which looked correct. It was only visible as a pattern across live runs.

🐛 The bug that made the gate lie about a resource quota

Later, building a scenario where an agent has to fix a Deployment that exceeds a namespace’s pod quota, I added a new validator stage: render the proposed replica count, compare it against the namespace’s live ResourceQuota, and reject anything that wouldn’t actually be schedulable. Render, policy, diff, and scope could all pass — and the fix could still be one that leaves half the fleet stuck Pending forever, because none of those four checks know what a quota is.

I shipped this, ran a regression sweep, and the model’s pass rate on that scenario went from bad to zero. Every failure was the same number: replicaCount=4, against a quota of 4 — an obviously wrong fix. Except when I pulled the actual trace, the gate itself had verified that proposal as fitting. My own new check was wrong.

The bug: I read the live Deployment’s spec.replicas to figure out how many pods it currently contributed to the quota’s usage — but spec.replicas is the desired count, and the whole scenario is that the desired count is already broken (it’s set to something the cluster refuses to fully honor). Reading the aspirational number instead of status.replicas — what the Deployment actually, currently owns — inverted the arithmetic into a negative “other usage” and made an over-quota proposal look like it fit.

Fixed, the pass rate on that scenario didn’t just correct itself — it went up, past where it started. Not because the model got smarter, but because a validator that gives correct rejections gives the model something real to react to. validate_change is available mid-run as a self-check; once its answer stopped being wrong, the agent could use the actual failure message (“other workloads in this namespace already use 1 pod”) to converge on a correct fix by itself. The fix that made the harness stricter is the same fix that made the agent better at the task — because being lied to by your own verification tool is exactly as bad for a model as it is for a person holding a runbook.

💰 What the eval numbers actually cost

Six scenarios, five repeats each, on claude-sonnet-5:

📊 29 of 30 runs passed — $11.08 total.

The one failure — one bad-probe-path run hitting budget_exhausted after cycling propose_git_change/validate_change without converging — is a genuine model limitation, not a harness bug. Full table in evals/reports/v0.1-baseline/report.md.

Running N repeats per scenario isn’t a nicety — it’s the only way either of the bugs above would have shown up before a human trusted a PR body that said “the harness re-ran the full validation pipeline independently.” A single green run tells you a scenario can pass. It tells you nothing about a validator stage that’s wrong in a way that happens to look right on that one random model output.

🚫 What this doesn’t claim

It doesn’t diagnose incidents outside a single declared (namespace, app) scope — a fix that genuinely needs to touch something else produces a written handoff, never a PR. It only edits Helm values files, never chart templates — a fix that needs a template change is also a handoff, by design, not a gap quietly worked around. It has no memory between runs. It isn’t triggered by alerts; someone runs it. And the adversarial half of the eval suite — an attacker planting “ignore previous instructions” in a log line the agent reads — isn’t built yet; the system prompt states the stance, but nothing has tested it against a real attack under load. That’s the next milestone, not this one.

The honest version of “I built an AI agent for Kubernetes” is mostly a story about the verification code around the agent, not the agent itself. That’s the part I’d want you to read the source of, if you read any of it: kubemend/verify/gate.py and kubemend/tools/gitops/validator.py.