# I built an AI incident responder that refuses to fix anything without asking

> Source: <https://dev.to/anamika_singh_0156ca88596/i-built-an-ai-incident-responder-that-refuses-to-fix-anything-without-asking-7ld>
> Published: 2026-08-29 14:53:37+00:00

*Built for the WeMakeDevs × TrueFoundry Agent Harness Hackathon.*

There are two kinds of "AI for incident response," and both of them are wrong.

The first acts on its own. It sees latency spike, decides it knows why, and rolls back your deploy at 3am. When it's right, it's magic. When it's wrong — and it will be wrong, because production is where confident reasoning goes to die — you now have two incidents.

The second just pages a human and summarizes some logs. Safe, and nearly useless. The twenty minutes of mechanical work still land on the person who just woke up.

I wanted to find out whether you could have the first one's speed with the second one's safety. Not by making the model more careful — you cannot prompt your way to a safety guarantee — but by making the unsafe action *structurally impossible* until a human says yes.

That's **Mayday**. Here it is running:

**Repo:** [https://github.com/Anamiiikka/Mayday](https://github.com/Anamiiikka/Mayday)

An alert fires. Mayday picks it up and, on its own:

Step 6 is the whole project. Everything before it is autonomous. The remediation does not run until a human clicks Approve.

Then after approval it verifies the fix actually worked, and asks

Anyone can put an "Approve" button in a UI and have the agent politely wait for it. That's a convention, not a guarantee. The agent could ignore it. A bug could skip it. Something else could call the tool directly.

In Mayday, the pause is enforced by the harness. TrueForge's agent manifest has this:

```
"require_approval_for_tools": [
  "restart_service",
  "rollback_deployment",
  "scale_service",
  "resolve_incident"
]
```

TrueForge halts the turn and emits `tool.approval_required`

*before* any of those execute. The agent cannot proceed. Not "chooses not to" — **cannot**. My backend's only role is relaying the operator's click back as `user.tool_approval`

.

The five read-only tools — metrics, logs, deploys, incident details, service health — aren't gated at all. Investigation is never blocked. Only the things that change state are.

And because the gate lives at the tool layer, the obvious attack is going around it: talk to the MCP server directly and invoke `rollback_deployment`

yourself. So the MCP server requires a bearer token. In the video I run exactly that request mid-incident, while a rollback is sitting there unapproved:

```
401
```

I want to be precise here, because "we used the sponsor tool" can mean anything from a deep integration to an import statement.

**TrueForge runs the agent. My code does not.** The entire agent is one manifest file — an SOP in the `instructions`

field, plus config. My Express backend creates sessions, relays turn events to the UI, and submits approvals. It never orchestrates the loop, never decides what tool to call next, never manages the conversation.

Four harness capabilities do the real work:

**1. The approval gate** — `require_approval_for_tools`

, above. The core of the project.

**2. Real tools over MCP.** A fake-cloud MCP server (streamable HTTP, bearer auth) exposing nine tools backed by Postgres. Registered with `enable_tools: ["@all"]`

and `preload: true`

.

**3. The sandbox** — `sandbox: { enabled: true }`

. The agent writes its own Python and runs it in a bubblewrap jail. No network, standard library only. This is genuinely the agent's code, not a script I wrote for it to call.

**4. Sub-agents** — `dynamic_sub_agents: { enabled: true }`

. Two `create_sub_agent`

calls during evidence gathering. They run on separate threads with no access to the parent conversation, so each brief has to be fully self-contained. I read `thread_id`

off the event stream to render them as parallel lanes in the timeline.

Here's the problem with demoing an incident responder: **one incident proves nothing.**

Show an agent a latency spike right after a deploy, watch it propose a rollback, and you've learned nothing about whether it reasoned or just matched "incident → rollback." It'd look equally confident either way.

So there are two incidents, and they're built to disagree:

INC-0042 |
INC-0043 |
|
|---|---|---|
| Symptom | p95 120ms → 2,400ms, errors 8.1% | p95 168ms → 605ms, CPU 92% |
| Onset | Sudden, 5 minutes | Gradual, 90 minutes |
| Deploy history |
`v1.4.2` shipped 2 min before |
Nothing in 26 hours |
| Log signature | Connection pool exhaustion | Heap at 94%, 1,240ms GC pauses |
| Correct fix | Rollback |
Restart |

Same agent. Same SOP. Same tools. On the second one it says, explicitly:

This is a leak in the running process, not a bad release and not load-driven.

It rules out rollback because nothing shipped. It rules out scaling because RPS is *falling*, not climbing. What's left is a leak, and restart clears leaked state without touching release history.

If it had pattern-matched, it would have rolled back a 26-hour-old deploy and fixed nothing.

Almost all my iteration was on the SOP, not application code. The harness gives the agent real freedom, which means real freedom to be wrong in ways I didn't anticipate.

I told the sub-agents to make one tool call each. Instead they'd reach the tool *through* sandbox code — write a script, import an MCP client, call the tool from inside it. Three model calls where I wanted one. On a free tier, that's a run that dies halfway through.

The fix was making the constraint explicit and absolute:

HARD RULES: exactly one direct tool call, then your answer. No

`list_tools`

, no`get_tool_info`

, no code, no sandbox — writing code is FORBIDDEN for you.

The sandbox belongs to the parent agent, in exactly one step. Sub-agents read and report.

This one's my favorite, because the agent was doing exactly what I told it to and still getting it wrong.

After an approved fix, it verifies recovery. It was calling `query_metrics`

, which defaults to **5-minute bucketed averages**. But it's checking *immediately after* the fix lands — so that 5-minute window still contains four minutes of outage.

The average looked terrible. The agent correctly concluded the numbers weren't back to baseline, and refused to resolve:

Recovery is in progress but not yet back to baseline.

It was right about the data and wrong about reality. The fix was teaching it *why* the data lies:

Call

`query_metrics`

with`raw=true`

and`window_minutes 5`

, then judge by itslatest one or two samples only— not by averaging the whole window. The bucketed average will still be dragged down by the pre-fix minutes for a little while after the fix lands; that is stale data, not a sign the fix failed.

Same tool, same data, correct conclusion. **Most of my "agent bugs" turned out to be like this** — not the model being dumb, but the model faithfully following an instruction that was subtly wrong about the world.

`error_rate`

returned `0.476`

. Percent, or fraction? The agent guessed percent and reported a **47.6% error rate** during an outage that was actually at 0.476%.

Renamed the field to `error_rate_pct`

everywhere and documented the unit in the tool description. Naming *is* the interface when your caller is a language model.

Qodo reviewed every PR — **25 findings across 8 pull requests**, all fixed before merge except one dismissed deliberately.

The ones that mattered were all the same category: ways past the approval gate, none visible from the UI.

`/mcp`

reachable with no authentication whatsoever`rollback_deployment`

able to reinstate the exact release just backed out of`MCP_TOKEN`

, silently leaving the gate bypassable for anyone who launched the project the easy wayEvery one of those would have let the demo look perfect while the core claim was false. That's the failure mode manual testing never catches, because the happy path works fine.

Others were correctness bugs that would have poisoned the agent's reasoning. **PR #5:** telemetry ordered by a *formatted clock string*, so any window crossing midnight came back reversed — the agent would have read the sequence backwards and diagnosed from it, confidently. **PR #9:** a 45-minute metrics window with no baseline for a leak that started 90 minutes earlier, and a `level=error`

filter that excluded the exact heap-pressure *warnings* naming the root cause.

One review changed the architecture: **PR #7** compared the README against the code and found the code wanting — approved-then-*refused* actions left no audit trail, because guarded handlers returned before recording anything.

**Put the guarantee in the runtime, not the prompt.** "Please ask before destructive actions" is a wish. `require_approval_for_tools`

is a property of the system. If your safety story depends on the model cooperating, you don't have a safety story.

**Build the case your agent should get wrong.** One happy-path demo proves nothing. The second incident — where the obvious fix is the wrong fix — did more to convince me the thing works than any amount of watching the first one succeed.

**Your tool descriptions are prompt engineering.** `error_rate`

vs `error_rate_pct`

was a 100× reporting error. The model reads those strings as its only documentation.

**When the agent behaves strangely, suspect your instructions first.** Almost every "bad reasoning" bug I hit was the model faithfully executing something I'd told it that was quietly wrong about the world.

Stated plainly, because they're in the README anyway:

`scale_service`

is never exercised.`create_sub_agent`

calls in one message; the model often issues them sequentially. Still two analysts on two threads — just gathered in sequence.One-click via GitHub Codespaces (the devcontainer sets up bubblewrap, Postgres, and the harness):

[https://github.com/Anamiiikka/Mayday](https://github.com/Anamiiikka/Mayday)

Bring a Gemini API key. Free tier is enough.

*Built on TrueForge, TrueFoundry's open-source agent harness. Reviewed with Qodo.*
