VPN Works 0.9.0 Cuts an AI Agent Off at Its Byte Budget and Records Why
VPN Works 0.9.0 gives Guard plugins byte budgets. A Guard lets a connection through with a budget, and the kernel counts every byte, cuts the connection when the budget runs out, and writes down why. The built-in Budget plugin uses it: a list of the hosts a program may reach, each with how much it may send, receive and open.
An AI coding agent with the network can send your repository anywhere. A policy already says where it may go. What it didn’t say until now is how much: an agent allowed to post to one paste site or one API could still push a whole archive through it.
Budget closes that. Give it the hosts, and the limits that make sense for each:

How It Works
vpnw guard --policy agent.toml --plugin budget --plugin ledger \
--set budget.allow='api.model.example up=2MB; *.npmjs.org down=500MB; github.com up=10MB requests=200' \
-- ./my-agent
A host on no entry is refused. For the rest, vpnw counts bytes on the connection itself, both ways. A connection that would pass its budget is carried up to it and then closed, and the record gets a connection.cut event naming the budget. Ask for another connection once the budget is spent and it’s refused before anything is sent. Names under a pattern like *.npmjs.org share one budget, so a program can’t get a fresh one from every subdomain it makes up. With *, any host is allowed with a budget of its own.
One more leak is closed on the way. With deny_private the policy looks names up to see where they point, and a DNS query can carry data out too. Now the Guards are asked first whether they’d refuse the name, so a name Budget refuses is never looked up.
Budgets aren’t a privilege of the built-in. They’re part of the Guard interface: any Guard can hand the kernel a budget for a connection it lets through, and the kernel does the counting. The plugin still never touches traffic. A Guard that hands over a budget vpnw can’t use is stopped, so a connection never goes through without the budget it was meant to have.
Exits Take New Clients Without a Restart
vpnw exit serve now looks at its file every 2 seconds, and at the policy files it names. Add a client and it can connect within seconds. Take one out, or give it a new token, and it’s refused and its open connections are closed. A file with a mistake is refused and the clients stay as they were; the exit’s record says why.
What the Tests Showed
The recorded session runs a stand-in agent under vpnw guard with Budget and Ledger. It called its API 3 times, all through. It tried to send a 5 MB archive to a host with 256 KiB up, and the connection was cut at exactly 262,144 bytes. The retry was refused, and so was a host nobody listed, which got nothing. Ledger sealed the cut, vpnw verify said the ledger was intact, and with the cut’s direction edited it named the changed line. 7 of 7 steps passed. The session
Six connections at once against an 8,000-byte budget delivered exactly 8,000 bytes between them. At an exit, a client added to the running exit’s file was let in after a second, and one taken out was refused with its open connection closed. Exit’s checks went from 21 to 23, all passed.
What the Review Found
An independent review went through the release twice and found six problems. Each is a test now.
- A removed client kept its connection. After any earlier reload, taking a client out of the exit’s file left its older connections open. Now every connection it ever opened is closed.
- A change during a reload got lost. A file saved while the exit was reading it could be marked as seen without being read. Now the exit hashes exactly the bytes it parsed.
- Refused names reached DNS. A name Budget refused could still be looked up by the policy first, which leaks through the query. Fixed as above.
- Guards decided what the policy refused. The first fix made Budget count connections the policy then turned down. The early question can only refuse now, and counts nothing.
- One address listed twice.
10.0.0.1and::ffff:10.0.0.1were taken as two entries, the second silently winning. - A policy file written late. A refused reload never noticed the policy file it was waiting for. Now it watches it.
Measured
| What | 0.9.0 |
|---|---|
| Agent session with Budget and Ledger | 7 of 7 steps passed |
| Exit checks, reload included | 23 of 23 passed |
Faults thrown by Lab, through --wireguard and rotate |
36, all handled |
| Test functions | 261 of 261 passed |
| Ways out of the sealed sandbox | 17 of 17 blocked, IPv6 included |
| Statements covered | 86.1% |
| Fuzz targets | 17, all clean |
How it was measured: test/results
What It Doesn’t Do Yet
Requests are connections: an HTTP client that sends many requests over one connection counts once. Budgets last for a run, or for as long as an exit serves. And the ledger still has no signed checkpoints, which is next in line. Plugins · Roadmap · Download