I Spawned 55 AI Agents By Accident and Other Stories From Building an Identity-Defense Weapon in 72 Hours
Or: how one human, ~50 AI sub-agents, and a graph database that occasionally changed its personality taught me what "production-grade" actually means.
The Pitch, or: Why I Built a Sword When Everyone Else Sells Maps
Every identity breach you've read about — Storm-0558, MGM, Snowflake — was not a hack. It was a walk. Somebody got one credential, opened Neo4j— sorry, opened their favorite graph tool, followed the pretty attack-path arrows to Domain Admin, and the arrows did not fight back. The tools that exist will happily draw you a beautiful map of your own destruction and then leave for lunch.
BloodHound shows you the path. Nobody swings the sword. And when a real SOC analyst finally hand-edits a group membership at 3 AM to break an attack chain, nobody re-measures anything, nobody writes a receipt, and the next auditor asks "how do you know that worked?" and everyone stares at the carpet.
So I built SEVER — an attack-path interdiction agent on FalkorDB. It ingests BloodHound-style Active Directory + Entra identity graphs, watches a findings stream, and computes the one edge whose removal kills the most attack paths to the crown jewels. Then — because I am not a monster — it refuses to do anything about it until a human clicks APPROVE and then CONFIRM. Two clicks. Deliberately annoying. Like a nuclear launch, but for Active Directory.
Then it deletes the edge on the live graph, re-queries reachability, and writes an append-only receipt: reachability 1 → 0, re-query 2.28ms, verdict: COLLAPSED, and the operator who approved it. The receipt is the product. Everything else is ceremony.
Working live demo: https://sever-console.onrender.com — go click things, it's real, the estate is a public BloodHound CE sample (PHANTOM.CORP, the most-breached fictional company in security research).
The Cast of Characters
- Me. Solo. Which the rules require you to announce, and which I want on record before you read about the next character.
- ~50 AI sub-agents across 9 fleets, disclosed per the rules and per basic decency. I directed; they executed; the commit history shows 86 merge commits authored by me, each with a reason. The rules say AI assistance must be disclosed, so here is the disclosure, with a witness: the AI also wrote this sentence, and it resents the implication that the humans did all the interesting debugging. Which, for the record, we did.
- FalkorDB, our database, which at one point spoke two different dialects of Cypher depending on which server it woke up on. More on this later. There will be a quiz.
- Render, our free-tier host, which restarts containers the way a cat restarts a glass of milk off a table.
Chapter 1: The Day I Asked for 10 Agents and Got 55
Early on, I wanted a 10-agent audit fleet. The delegation API has a hard cap of 10 tasks per call. My first payload had 11. It got rejected. Fine. I fixed it. And by "fixed it," I mean my fix was also 11 tasks, five times in a row, and the runtime — which had been quietly counting — hit a guard: this turn has already spawned 55 subagents (limit 50).
I had spawned 55 agents while trying to spawn 10. This is the closest I have ever come to a fork bomb with a spending limit. Somewhere in a data center, 50 AI agents were all simultaneously deciding to inspect the same repository, like a flash mob for people who love pytest.
The lesson: when a machine tells you "max 10," it is not a suggestion, and it is counting. Unlike your gym membership.
Chapter 2: The Merge That Ate My UI (and How I Learned to Love Atomic Conflict Resolution)
At one point, five separate agents had each redesigned the same HTML file. Each one was individually brilliant and collectively a war crime. Merging their branches produced 31 conflicts — in one file. Not 31 lines. 31 conflict regions. The file was roughly 60% conflict markers by volume, which is a sentence that should never be true of anything.
I resolved them by hand, one by one, deciding which agent's design language won where — like a divorce lawyer for CSS. Then a vision-check of the merged result caught that the animation constants were broken: a missing variable, a dead element ID, and a UX rule that the banner could overwrite your cut receipt with a "partial data" notice. All three shipped bugs were mine, from the merge, not the agents'. The agents' code worked. My glue didn't. Humbling — like building a car from five perfect engines and discovering you've connected them with shoelaces.
We fixed it, wrote regression tests pinning the exact failure modes, and moved on to never doing that again: every subsequent fleet got isolated git worktrees and single-file ownership, so merges became ceremonial. Boring is a feature. Boring is the feature.
Chapter 3: Designing a Logo with a Blind Art Critic
The brief: "a great logo, very deep and very unique."
The journey:
- v1: A severed letter S. My reviewer (a vision model I was using for blind QA) rated the concept a 4/10 and described it, unkindly but accurately, as "a striped S."
- v3: Two flat cut-faces, one clean diagonal void. "Reads as a dashed line," said the critic. "The sever is implied. Like an apology."
- v6: The blade was inside the ring. "A gem in a bezel," said the critic. "Zero cut semantics." It had trapped my knife inside the target like a decorative hostage.
- v9: The vision model calmly informed me my logo now resembled the Go golang logo. There is no recovering from being told your edgy security mark is cuddly mascot-adjacent.
- v10: "Resembles IntelliJ IDEA." My logo was now a JetBrains product.
- v12: The severed ring, welded: arcs that end inside the red blade, jewel mounted on the surviving arc. 9/10 — "Ship it."
Twelve versions, one vision model, zero human designers. The final mark literally tells the product story: the attack path (a ring) is severed by one precise blade, and the crown jewel rides the surviving arc. The AI critic earned its keep — every rating was an actionable diagnosis, not a vibe. I have worked with human designers who gave less usable feedback, and I have been the designer giving less usable feedback.
Chapter 4: F900 — The Bug That Was Working Correctly
The night before final polish, production "broke." Click "Simulate breach finding" and the console said the attacker could reach... zero crown jewels. Empty paths panel. Panic. Three separate fix attempts by three separate agents, each addressing a plausible engine-dialect theory. A forensic deep-dive that exonerated the engine, the queries, the client library, and the network — each verified live, with receipts.
Then the actual answer arrived: my own demo-video script had clicked APPROVE → CONFIRM on the top-ranked edge — the exact max-coverage bottleneck edge — and the engine had correctly collapsed the estate. Reachability zero. Paths gone. The product was not broken; the product had won, on camera, and left me a forensic trail of receipts explaining exactly how it had won, which I was too busy debugging to read.
The bug was filed as "production reachability broken." It should have been filed as "product defends itself against owner; owner surprised." I spent hours debugging my own product's success. Somewhere, a skeptic of AI-written software is reading this and feeling very validated, and I want them to know: the receipts said COLLAPSED the entire time. The receipts are the product. Read the receipts.
(Lesson industrialized: never diagnose against an estate your own scripts mutated. Fresh reload, re-verify, then panic.)
Chapter 5: The Database That Spoke Two Dialects
FalkorDB Cloud ran engine v4.22.0. Our dev box ran 6.0.0. Same product, same version family — and, we discovered, not the same Cypher dialect. A WITH shortestPath(...) query form that runs clean on 6.0.0 returned a single row containing null on 4.22.0 — not an error, not an empty set, just a serene little null, the database equivalent of answering a question with a shrug.
Meanwhile the cloud engine rejected a batched query form with a syntax error at column 741, inside a 43-relationship alternation list, and the error mentioned relationship names that the dev engine accepted happily. Two engines, two personalities, one codebase.
The fix: probe the engine version at first contact, fail safe to the older dialect, and verify the fallback on the live cloud — because the local test suite physically cannot catch a dialect that only exists in production. The test that guards this runs the real engine, or it doesn't run. Everything else is theater.
Every database has a dark side. Ours was polite, well-documented, and null-shaped.
Chapter 6: The Byte That Broke the API
A security product should survive its own users, so one audit agent threw the whole keyboard at the API: unicode RTL overrides, lone surrogates, emoji, 2^70 as an integer, JSON with byte-order marks. Almost everything failed closed and politely, per spec — 422, 400, readable errors.
Then one agent sent a single NUL byte inside a principal name. It sailed through the schema, hit the engine as a query parameter, and the engine responded with an error our outage handler didn't classify. Bare 500. The kind of thing that ends up in a CTF write-up, except we were the target.
We fixed it the right way: control characters now die at the schema, 422, before any mutation, with a regression test file dedicated to this exact shame. There is now a test in the repo whose entire purpose is to assert that a NUL byte gets told no. A single test, standing in the darkness, so that no byte may pass unnamed. I have written worse monuments.
Chapter 7: The Health Check That Ate the Ledger (Four Times an Hour)
Our free-tier host health-checks every few seconds and requires a 200 within five. Our /api/state endpoint runs several engine queries, and under cloud load it took 6-24 seconds. So the health check failed, the host restarted the container, and the restart wiped the ephemeral receipts ledger. Four times in 47 minutes. We watched our own append-only audit trail get append-deleted on a schedule you could set a clock by.
The fix had layers, like an onion, or a parrot: a /api/health endpoint that runs one cheap query and writes no receipt; the receipts route bounded to the newest entries so it can't 502 the worker by shipping megabytes of history; and the ephemeral-filesystem reality documented in the README — honestly — because lying about persistence in a security receipts product would be a fun headline for somebody else.
The lesson: free-tier infrastructure will find your product's assumptions and eat them. Design for the tier you're on.
Chapter 8: The Slowest Fast Button in the West
A user clicked "Plan k-cut sequence" and the UI said "planning..." and then nothing, for thirteen seconds, which on the internet is a geological era. The button wasn't broken. The problem was arithmetic: 197 round-trips from the console to the cloud engine, at ~65ms of WAN latency each. The algorithm was fast; the distance was slow.
We re-derived the whole path: fold redundant queries (197 → 11), batch the per-pair enumerations into one server-side statement, pipeline the independent reads, and overlap the copy with the first measurement. Result: the same plan, in about two seconds — with an equivalence test suite proving old and new return identical results, because the only thing worse than slow is different.
Now the UI also shows elapsed time and a real progress bar while it works. Turns out "the engine is thinking" is a message users find reassuring, whereas silence makes them assume you've stopped paying your hosting bill. Correct on both counts.
Chapter 9: The Rules Audit, or: Every Paranoia Was a Receipt
Before going public, I ran a full rules-compliance audit — six auditor agents against the eighteen official rules, with fixer agents patching every finding live. The receipts:
- Zero secrets in the entire git history. All 893 objects, every branch, every commit message, scanned. Nothing. The one password in the docs was its length being mentioned, which was itself a finding (fix: stop telling people the password is 7 characters; also: rotate it, because it was my laptop unlock password, which is a sentence that no security professional should ever type).
- Every performance number in the README traces to a JSON receipt. The auditors found claims that were stale in our favor — docs quoting pre-optimization numbers when the current ones were 3-9× better. Understatement as a bug. We fixed the docs to match the receipts, not the receipts to match the docs.
- A test-count badge that said 563 when the receipt said 454. The 563 was real — but unreceipted. In this repo, if a number has no receipt, it does not exist. There is now a fresh receipt. The badge and the receipt agree. The ledger and the badge agree. Peace.
If you take one thing from this post: receipts or it didn't happen. Not in the marketing sense. In the "here is the JSON file with the median latency, generated by the script in the repo, re-runnable by you" sense.
Chapter 10: What the AI Actually Did (and the One Rule That Mattered)
The rules require disclosure, so here it is with receipts. This project was built during the hackathon window (first commit at 00:02:53 IST, window opened at 00:01 — we are nothing if not prompt) by me directing AI agents under explicit gates. The evidence that a human was home: 86 merge commits, each authored by me, each with a stated reason — "merging the atomic-cut fix because the count-then-delete pair double-claimed under a race, reproduced 6/6 rounds." The agents wrote code; I decided what was true, what merged, and what the product believes. That's not a disclaimer, that's the job description — and the product's own two-step human gate is the philosophy wearing a UI.
The failures in this post were my glue, my flags, my 11-vs-10 counting, my own demo script collapsing my own estate. The successes belong to the loop: human sets the bar, agents clear it, receipts keep everyone honest.
AI did not build this product. A human with 50 power tools and a receipt printer built this product. The power tools just don't sleep.
The Receipts
Everything below is a file in the repo you can re-run:
- Estate: 1,480 nodes / 12,450 edges, 5 crown jewels, hero cut re-queried at 2.28ms
- Scale wall: 300 tenants / 444,000 nodes / 3,735,000 edges — 4.67ms median interdiction query at 1.59 GiB RSS
- Ablation: graph engine solves 5/5 interdiction cases; keyword search 0/5. The graph is load-bearing, per rule and per physics.
- Two-arm reachability: 3.1-3.6× faster than the per-pair form, equivalence-proven on three estates
-
Tests: 580 collected, 0 failed (receipt:
docs/receipts/test_suite_2026-10-17.json)
The Links
- Live console: https://sever-console.onrender.com — click "Simulate breach finding" and watch a threat die honestly
- Source: https://github.com/choksi2212/wemakedevs-hack — every claim above, plus the mermaid diagrams, the Cypher queries, and the incident post-mortems
- Demo video: 3 minutes — the live arc plus the product explained with motion graphics
SEVER: sever the path, prove the collapse, read the receipts. Preferably before assuming the product is broken.
Built solo with AI assistance, disclosed per Rule 11 and lived per Chapter 4.
Top comments (0)