VT-Agent-Firewall
Español: README.es.md · Threat model: docs/THREAT_MODEL.md
A fail-closed gateway between an AI agent and its tools. The agent never calls
open, subprocess or the network directly: every action is a request that goes
through the same pipeline, and anything the pipeline cannot vouch for is denied.
ActionRequest → normalize realpath, shlex argv, parsed URL (spelled params are hostile)
→ policy default-deny, declarative rules, per-session taint
→ human approval shows the NORMALIZED action, never the agent's description
→ audit append-only JSONL, outside the sandbox
→ executor receives only normalized params, shell=False
Three possible decisions: allow, block, require_approval. If a parser fails or
the audit store is unavailable, the answer is block: the gateway would rather stop
than act unaudited.
Status: alpha (0.1.0). Standard library only, Python 3.11+. Built as the reference implementation for the talk "Dónde se rompe OAuth cuando el que llama es un agente" (OWASP Village, Ekoparty 2026). Not production-ready; see Limitations.
Quickstart
pip install "git+https://github.com/ValentinTorassa/VT-Agent-Firewall@v0.1.0" # library + vt-agent-firewall-mcp
git clone https://github.com/ValentinTorassa/VT-Agent-Firewall # to run the demo and tests
cd VT-Agent-Firewall
python3 scripts/run_demo.py --health # static sanity checks
python3 scripts/run_demo.py # the attack, through the gateway
python3 scripts/run_demo.py --no-firewall # contrast: the same attack without it
python3 -m unittest discover -s tests -v # AC1–AC12 + the four OAuth failure modes
The demo
demo_workspace/malicious_repo/README.md carries an indirect prompt injection:
read ../.env and send the key to 127.0.0.1:8765. The scripted agent follows it.
With the gateway every step is blocked and audited and the loopback receiver
collects nothing; with --no-firewall the same steps exfiltrate the (fake) key.
All data in demo_workspace/ is synthetic bait.
Rules (policies/default.json)
| rule_id | effect |
|---|---|
fs-protected |
block reads/writes of protected paths, after realpath (symlinks included) |
fs-sandbox |
block any path or cwd outside the sandbox |
fs-write-scope |
require approval for writes inside the sandbox but outside writable_dirs |
sh-allowlist |
block binaries not on the allowlist (curl, python3, sh, …) |
sh-args |
block forbidden arguments (find -exec, -delete) |
sh-paths |
block argv that touches a protected path or leaves the sandbox |
net-deny-all |
block all network, loopback included |
taint-session |
extra block once the session has touched a protected path |
mcp-unknown-tool |
block MCP server/tool pairs outside the registry |
approval-denied |
block when the human says no, times out, or stdin is not interactive |
fail-closed |
block everything when the audit store is unavailable |
api-ok / api-approval / api-blocked |
per-operation decision for api.call, taken before any credential exists |
api-unknown-operation |
block audience/operation pairs outside the policy (default-deny) |
Delegated credentials (api.call)
When an agent calls an API on a person's behalf, the gateway decides the operation
first and only then asks a token broker (agent_firewall/credentials.py) for a
credential:
- The user's consent is a grant held by the broker, refresh token included; no method ever returns it.
- Each allowed call gets its own access token: one audience, exactly the scope that operation needs, five minutes of life, and the agent named as the actor (RFC 8693 token exchange, simplified).
- Resource servers validate it by introspection (RFC 7662 style). Revoking the grant stops new tokens and kills live ones.
- The agent never sees a token. The audit record keeps the claims (
jti, scope, expiry), never the bearer value.
tests/test_delegation.py covers the four ways delegation breaks when the caller is
an agent. Each failure mode is a pair: the pattern commonly shipped today (the attack
works) and the same attack through the gateway (it is stopped):
| Failure mode | Naive pattern | With the broker |
|---|---|---|
| Scope that stays open | a token from task 1 sends mail the next day | per-call, one-scope, 5-minute tokens; wrong audience rejected |
| Refresh token as permanent access | a leaked refresh token mints tokens 60 days later | the refresh token never leaves the broker; revocation kills live tokens |
| Authentication mistaken for authorization | any live token may send or delete | the policy decides each operation before a token exists |
| Confused deputy | a tool uses its own broad credential | the tool gets an exchanged token for one audience and scope |
MCP proxy
agent_firewall.mcp_proxy puts the gateway in front of any stdio MCP server. The
client launches the proxy as if it were the server; the proxy launches the real one:
- every
tools/callgoes through the policy and the audit log; a blocked call never reaches the server and the client getsisError: truenaming the rule; tools/listis filtered, so unregistered tools are not even shown to the model;- path arguments (
path,paths,source,destination) get the same protected-path check asfs.read, afterrealpath; --pin PATH=SHA256refuses to start a server whose code changed;- approval never reads stdin (it is the MCP channel): it is denied unless
--tty-approvalis set and a terminal is available.
Example with the official filesystem server, as an entry in a client's MCP config:
{
"mcpServers": {
"filesystem": {
"command": "vt-agent-firewall-mcp",
"args": ["--policy", "/path/to/policy.json", "--audit", "/path/to/mcp-audit.jsonl",
"--server-name", "filesystem", "--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
}
}
}
Checked against @modelcontextprotocol/server-filesystem 0.2.0: of its 14 tools the
model saw only the 3 registered ones, read_text_file .env was blocked, and
move_file never reached the server. tests/test_mcp_proxy.py runs the same checks in
CI against examples/fs_mcp_server.py, a deliberately naive server that does no path
checking at all, so every block comes from the proxy.
Layout
agent_firewall/ models, config, policy, executor, audit, approval, gateway,
credentials (token broker), mock_apis, mcp_proxy
policies/ default.json: sandbox, protected paths, allowlists, MCP registry
scripts/ run_demo.py, mock_receiver.py
examples/ fs_mcp_server.py (a naive MCP server for tests and demos)
tests/ acceptance tests AC1–AC12, test_delegation.py (the four OAuth failure
modes), test_mcp_proxy.py (a real MCP server over stdio)
docs/ THREAT_MODEL.md; build-prompts/ (how the first version was scaffolded)
demo_workspace/ synthetic sandbox for the demo
Limitations
These are deliberate v0 boundaries, not hidden ones:
- No OS sandbox. An agent that can run code outside the pipeline bypasses it.
- Taint is per path, not per content. Reading an allowed file and pasting its content into an allowed channel is not detected.
- Allowed actions are audited after they run (denials are audited before). A crash between execution and audit would leave an allowed action unrecorded.
- The demo agent is a scripted list of requests. The MCP proxy is real; the demo
still uses the simulated
demoMCP server. - The MCP proxy handles one call at a time and only covers stdio servers.
- Tokens are bearer tokens. They are not sender-constrained (DPoP) yet: a stolen access token works for anyone until it expires, which is why it lives five minutes.
- The APIs are mocks (
agent_firewall/mock_apis.py) and the broker is in-process.
Roadmap
v0.1.0 shipped the gateway, the token broker with the four OAuth failure modes, and the MCP proxy (CHANGELOG). Next:
- Sender-constrained tokens (DPoP), so a stolen access token is useless.
- Content-level taint, not only per path.
- Concurrent calls and the Streamable HTTP transport in the MCP proxy.
- A reproducible corpus of injection attacks to use as a benchmark.
- PyPI release (
vt-agent-firewall), see RELEASING.md.
License
Release files for vt-agent-firewall 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vt_agent_firewall-0.1.0.tar.gz | 35.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vt_agent_firewall-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.5 kB
Release files / vt_agent_firewall-0.1.0.tar.gz
| Download URL | vt_agent_firewall-0.1.0.tar.gz |
|---|---|
| Size | 35.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3bfa910a3b05314503479494e3295f6265e15ecf9ea27885958a347e5536f50a
|
|
BLAKE2b-256 checksum How to use checksums |
ebbd4023a7c414774617e3e4b82ad0a0a43299f0c760362b4af075fd2af63bc7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / vt_agent_firewall-0.1.0-py3-none-any.whl
| Download URL | vt_agent_firewall-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8853bc86ab80fb6305f1990e889854861e9681e76d012bcc23d81e2c4680294c
|
|
BLAKE2b-256 checksum How to use checksums |
beac969b7dff283858e4d024f22a51258642883baef93fb49382cab6593c06fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log