VORIM
We use cookies

We use cookies to analyze site traffic and improve your experience. You can choose to accept all cookies or only essential ones. See our Privacy Policy.

SecurityRuntime ControlEngineering

Our Kill Switch Stopped One Agent. Its Workers Kept Going.

On 9 October we ran a code-level audit of our own runtime, the part of Vorim that decides whether an agent's next action is allowed. We wanted to know what an enterprise security team would find in a proof of concept before they found it. The answer was uncomfortable enough to write down.

The headline finding is in the title. Suspend is our kill switch, and it worked exactly as described for the agent you pressed it on. Its next permission check failed, its cached decisions expired, and the Shared Signals event went out. Every agent that orchestrator had delegated to carried on, because each of them was being checked against its own grants, and nothing in the suspend path looked at the delegation tree. A compromised orchestrator's workers would have kept acting until someone suspended them one at a time.

That was not the only thing. Below is the rest, in the order we fixed it. All of it is live now.

What the audit found

The problems grouped themselves. Several controls trusted a number the caller supplied. Token budgets charged whatever estimated_tokens the agent declared, and a workflow budget was keyed on a free-text workflow_id that an agent could change at will. Reliability inside the trust score counted only audit events whose signature the server had verified, which sounds right until you learn that the verification code only fetched the agent's public key when an environment flag was set. With the flag off, which was the default, no event was ever marked verified, so the reliability term had no input at all. The code had been that way since June.

A second group of controls stopped at one agent. Suspend did. Behaviour windows did too, so the segregation-of-duties rule that stops one agent from both initiating and approving a payment could be beaten by splitting the two steps across two agents. Splitting work across agents escaped the budget, the velocity check and the SoD check at once, which is why the fix had to be one thing rather than three patches.

A third group was off or open by default. The org setting that decides what happens when the decision engine times out defaulted to fail open, and so did the SDKs, so anyone who could slow the engine down got an allow. The escalation timeout was stored, validated, described in a comment as "auto-resolved as denied", and read by nothing; a stale escalation could be approved days later. Alert webhooks were sent once with no retry and never looked at the response code.

And one group was simply missing. There was a working API for the delegation graph and a live per-chain WebSocket room, and no page in the dashboard used either. You could not see a swarm of agents, only a list.

The fix needed a new identity

Every control that keyed on a group of agents needed a group identifier the agent could not forge. workflow_id could not be that identifier, because the agent sends it. The delegation tree already had a server-minted id, but nothing outside the audit log carried it, and an ephemeral worker spawned for an orchestrator had no delegation link at all.

So Vorim now mints a swarm id. It is created on the server the first time a root agent delegates to another agent or spawns an ephemeral worker, every member inherits it when it joins, and each member records which agent it joined under. The runtime stamps it onto every decision, every audit event and every shared-memory write from the agent's own record, never from the request. In a signed audit bundle it travels in the attestation envelope, not the signed body, so existing signatures still verify.

With that in place the other fixes followed. A token budget can now be set for a swarm, and the check reserves the estimate in a single atomic Redis call so concurrent actions cannot all pass on the same stale count. A policy rule can read the whole swarm's behaviour window instead of one agent's, so the SoD rule fires when the two duties were split across two agents. And suspend cascades. Suspending an agent walks its delegation links and parent pointers, suspends every active agent beneath it, expires their cached decisions, and sends each one its own webhook and Shared Signals event, because a receiver that maps agents to sessions needs to revoke the children's sessions too. Reactivating the root brings back exactly the agents that cascade stopped, and stops at any agent an operator suspended on their own.

Before you press it, the dashboard shows what will happen. The preview lists every agent in the subtree, how many are still active, and how many open escalations will time out. The same preview backs the new Swarms page, where you see each swarm as a unit with its members, its live delegation links, its recent decisions, its budget and a single kill switch.

The smaller fixes

Fail closed is now the default for new organisations and for both SDKs. Existing settings were left as they are, because flipping a customer's running agents without them choosing it would be its own incident. Unanswered escalations now time out as a platform-signed denial, swept every minute; the first sweep after deploy timed out five of our own demo escalations that had been waiting since September. Webhooks retry on network errors and 5xx responses, with backoff. The public RFC 9421 verifier accepts a signature once per max-age window rather than for five minutes on repeat, and the A2A middleware gained a challenge store so a captured proof-of-possession cannot be replayed. Signature verification runs on every signed event regardless of the flag; the flag now only decides whether a bad signature is rejected or stored as unverified.

We also stopped charging token budgets for escalated actions until a human approves them, made a hard budget fail closed when the counter is unavailable instead of silently switching off, and started writing the live trust score through to the stored column the dashboard reads, which had been seeded at 50 and never updated.

What we would say to anyone building this

Write down which inputs to each control the agent itself supplies, and assume they are hostile. Then take each control and ask what happens when the work is split across two agents, because that is the cheapest evasion there is and it beat four of ours in one move. Read your defaults as an attacker would, since an attacker who can slow your engine or block the path to it is choosing your fallback for you. And put the group on a screen, because a control nobody can see is a control nobody notices has stopped working.

None of this is research. Each item falsified something our product said about itself, and the whole set shipped in a day with a test for every fix. If you run agents that delegate or spawn workers, the swarm view and the cascading kill switch are in the dashboard now.

See the swarm kill switch on your own agents →

Questions, we read every email at team@vorim.ai.

Found this useful? Share it.

Share
Get started

Ready to build with AI agents?