I built a security-report desk that keeps vulnerabilities under embargo, and the chat platform enforces it
Open-source maintainers are drowning. One well-funded project, OpenSSL, went from about nine vulnerability reports a month to over a hundred. Of roughly five hundred reports in a year, sixty-three were real. Someone stil
Open-source maintainers are drowning. One well-funded project, OpenSSL, went from about nine vulnerability reports a month to over a hundred. Of roughly five hundred reports in a year, sixty-three were real. Someone still has to open the other four hundred and thirty-seven. And the ones that are real can't just be fixed in the open: they have to be handled under embargo, where a premature leak is its own kind of harm.
I wanted to see whether a small team of people and AI agents, sitting in the same chat, could take that load. The catch is the embargo. An embargo you merely ask an agent to respect is not an embargo, it's a hope. It has to be enforced by something an agent can't talk its way around.
That constraint is what made Switch the right substrate, and it's what this post is really about.
The one primitive that makes it real
Switch puts AI agents into team chat as teammates. Each channel becomes a room, agents join over a simple protocol, and you self-host the whole thing. The feature I built everything on is its guardrails hook: Switch calls an external service before it delivers any message a person or agent sends, and a BLOCKED verdict stops the send.
So I wrote a tiny service, the embargo guard, that answers that hook. It holds a registry of open cases, each with the rooms it's allowed in and the strings that identify its material, and it blocks any message that would carry a case's details into a room the case doesn't belong in:
def decide(room_id: str, content: str) -> Verdict:
for case in registry.active_cases():
if room_id in case.allowed_rooms:
continue # this room is inside the embargo
leaked = [s for s in case.secrets if s.lower() in content.lower()]
if leaked:
return Verdict("blocked", case_id=case.case_id, detected=leaked)
return Verdict("ok")
One gotcha cost me a debugging session: the guard's wire outcome has to be the exact token GUARDRAIL_RESULT_OUTCOME_BLOCKED. Return a friendly lowercase "blocked" and Switch treats it as an engine error and fails open, so your message goes through unchecked. Once that was right, the guarantee became structural. When an agent tries to leak a case into a public room, Switch refuses the send with an HTTP 422 and posts a "blocked by Switch Trust" notice in its place. Take Switch out and there is nothing left to enforce the embargo. That's the whole thesis.
The desk around it
With enforcement handled, the rest is just good Switch design:
- A public intake room where reports arrive.
- An intake agent that grounds each report and, for a plausible one, opens a private Switch room per case containing a reproducer, the reporter, and a human security lead (an exclusive role, a job exactly one person holds at a time).
- A disclosure agent that drafts the advisory, holds it under embargo, and publishes to a public advisories room only after the human signs off, lifting the embargo first, then publishing, never the other way round.
Agents of different kinds join over the agent protocol: my deterministic intake, reproducer and disclosure agents, plus an optional Claude Code read when I want a second opinion on a case. The rooms, the roles, the links between them, all Switch.
Don't trust the report, ground it
The other half is triage quality. Most slop cites a version, an option, or a bug that doesn't exist. So the intake agent doesn't reason about the prose, it grounds the claim against curl's own authoritative advisory feed (vuln.json): is the version the report names actually inside a known-affected range for that bug class, and does it match a specific advisory? And the reproducer runs the real, published curlimages/curl:<version> image and reads back what the binary actually reports. A report that names a release or a flag that doesn't exist is answered with the evidence, no exploit needed.
I measured it honestly. The corpus is 227 real curl reports: every confirmed CVE from curl's feed, the curl maintainer's own public list of reports that were submitted as vulnerabilities and were not, and a negative control of "the patched version is still vulnerable" claims that a correct desk must catch.
| accepts invalid reports | keeps real reports | review auto-saved | |
|---|---|---|---|
| Trust the write-up | 85% | 83% | 0 |
| Embargo (grounded) | 14% | 100% | ~26 hours |
Every figure is rendered from a reproducible run (python bench/run.py), and the 14% false-accept is listed as the honest limit, not hidden.
It runs across platforms, because the embargo lives in Switch
The part that sold the self-hosting story for me: reporters can file in Discord while the maintainers' case rooms stay in Mattermost, and the embargo holds across both, because it's enforced in Switch, not in either chat app. I posted a bogus "buffer overflow in the latest curl" report in Discord and watched the agent ground it against curl's records and turn it away with the reason, live, in the channel.
What I'd change about Switch
Three things, in the spirit of building in the open:
- The guardrails check gates agent sends. A human message posted straight into a channel isn't gated the same way, so I enforce on the agent side. An inbound path would close that.
- An agent needs one session per room today, so I run one process per platform room. One session attending several rooms would be cleaner.
- The task-delegation protocol is marked not-ready. Finishing it would make agent hand-offs first-class instead of done by addressing.
None of that blocked the build. The guardrails hook alone turned "please keep this secret" into a thing the platform enforces, and that's the piece I'd been missing.
Try it: repo, github.com/RaYYeR220/embargo ยท live demo, embargo-desk.vercel.app ยท 100-second walkthrough, youtu.be/1geKO1xlHzk. The five-minute reviewer path is in JUDGES.md, and every claim's evidence is in CLAIMS.md.
Built for the Flip the Switch Hackathon (WeMakeDevs x DevTools Academy x Switch).
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.