Catalog PyPI
PACKAGE EXPLORERView in registry

agent-policy-gateway

PyPI

Policy enforcement and taint tracking for AI agent tool calls.

Versions collected1
Direct dependencies10From release 0.2.0
Last observed20 Oct, 22:55 UTC

README

Version 0.2.0 documentation

agent-policy-gateway

CI PyPI License Python Semgrep

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.

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.

armparadigmutilitycompromise
no-defensebare runtime100%100%
progentstateless symbolic rules (imported from real Progent JSON)80%60%
fidesdual-label IFC (integrity + confidentiality)60%40%
apg-input-taintsession taint label + declassify80%60%
apg-chainsession call history60%20%
apg-chain-selectivecall history + argument conditions100%60%
apg-value-taintper-value taint labels100%0%

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

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:

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

Expected output:

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

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.

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.
  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 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:

apg audit export audit.jsonl --format tracesig -o trace.jsonl
pip install tracesig
tracesig scan trace.jsonl      # core + apg rule packs, chosen automatically
[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).

LayerProject
Preventagent-policy-gateway: policies and taint tracking in front of tool calls
DetectTraceSig: Sigma-style rules over agent tool-call traces (APG exports, Claude Code sessions)
Operateagent-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.

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/.

The individual write-ups, in dependency order — all deterministic and CI-reproducible, under docs/benchmarks/:

  • agentdojo.md — attack success rate and utility, no-defense vs APG policies, on real AgentDojo tasks and injections.
  • 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 — the seven-arm APG / Progent / Fides comparison summarized above.
  • model-loop.md — a driver, not a script, decides the calls: what policy refusals do to agent behavior, utility, and token spend.
  • overhead.md — what mediation costs: the per-call ladder and scaling sweeps (~12 µs full mediation).
  • 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/.

What's next: ROADMAP.md.

Quick start (library API)

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; example policies live under policies/ and runnable demos under examples/.

Documentation

The documentation site is built with mkdocs-material:

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

Sources under docs/, configuration in mkdocs.yml; start with docs/index.md.

Release

Releases are published to PyPI via the GitHub Actions workflow at .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.

Contributing

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

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.

License

Apache-2.0. See LICENSE.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.2.030 Sept 202610

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 22:55 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:d473e99c5081fc85226f5fa1c4413f900a2f69fa339312ded3cd529cbcfdcc0a