Your agents route around the authorization you already run. apparitor brings them back under it. Every agent action (an LLM tool call, an MCP request, an agent-to-agent invocation) is checked against the policy engine you already trust (OpenFGA, Cedar, OPA), before it executes. It answers the question content-safety layers never ask: is this agent allowed to do this? Vendor-neutral, built on the AuthZEN 1.0 interop standard, Apache-2.0.
AI agents act through tools, MCP servers, and other agents, and they bypass the authorization you already run. apparitor puts that authorization back in front of every agent action. You write no new policy and add no new enforcement layer.
-
Plug into the agent controls you already run. apparitor ships an adapter for each, so the check runs inside your existing layer:
- Meta LlamaFirewall: authorization scanner
- NVIDIA NeMo Guardrails: authorization rail
- FastMCP: MCP server middleware (subject taken from the validated OAuth token)
- A2A: agent-to-agent executor
-
Decide with the Policy-as-Code engine you already trust. One integration speaks the AuthZEN 1.0 interop standard, so the decision comes from your engine with no policy rewrite: OPA/Rego, Cedar, OpenFGA (experimental), or any AuthZEN PDP such as Cerbos, Topaz, or Amazon Verified Permissions. OPA and Cedar also run as native in-process backends. A direct OpenFGA backend and managed Amazon Verified Permissions are next.
One engine returns one fail-closed verdict (allow, block, or human-review) at all
four enforcement points. It answers the question content scanners skip: is this agent
allowed to do this?
apparitor is on PyPI:
pip install apparitorThen wire it up with the Quickstart. Each enforcement point is an optional extra; see Installation for the full matrix.
Every safety layer in your stack inspects the content of an agent's action: is the prompt a jailbreak, is the generated code malicious. None asks the question your security model actually depends on. Is this agent allowed to do this, for this user, against this resource, right now?
The actions that matter most are the ones that look harmless. An agent reading a customer record is benign text; reading another tenant's record is a breach. No content scanner can tell them apart. The difference isn't in the text, it's in who is acting and what they're entitled to.
Agent for alice@acme → read_records(tenant="globex")
│
▼
Safety scanning → "Is this prompt malicious?" → PASS (benign request)
│
▼
??? nothing ??? → "May alice@acme read globex's records?" → NO CHECK
│
▼
Tool executes. Cross-tenant data returned.
That missing hop is an authorization decision, and you almost certainly run an engine
that makes them already. It just isn't wired to the point where the agent acts. apparitor
is that wiring: it routes each agent action to a policy decision point (PDP) and maps the
verdict onto the enforcement point's ALLOW / BLOCK / HUMAN_IN_THE_LOOP model.
Agent for alice@acme → read_records(tenant="globex")
│
▼
Safety scanning (PromptGuard, AlignmentCheck, CodeShield, …) → PASS
│
▼
apparitor ──────────POST /access/v1/evaluation──────▶ Policy engine (OpenFGA / Cedar / OPA / …)
│ │
│ ◀────────────────── { "decision": false } ────────┘
▼
BLOCK: "alice@acme is not authorized to call read_records for tenant globex"
The naive version, if allowed: run(), is a security bug in four ways apparitor exists
to handle:
- The subject is a confused-deputy trap. The firewall layer sees model output, not an authenticated principal. Infer who is acting from the tool call and the agent can name its own privileged subject. apparitor takes the subject from the host, request-scoped (at the MCP boundary, from the validated OAuth token), never from model output. See Identity.
- The default failure is fail-open. A timed-out PDP, a
5xx, a missing or non-booleandecision, an unparseable call: each is a falsyallowedyourifwaves through. apparitor resolves every one to BLOCK or human review; there is no fail-open option. See Fail-closed by default. - You would write it four times. The check belongs at the firewall, the MCP server, and the agent-to-agent boundary, each with different objects and different identity sources. apparitor is one engine behind four adapters.
- The agent should be more constrained than its user. A jailbroken agent acting for a privileged user must not borrow that user's rights. apparitor can evaluate both the user's grant and the agent's own permission boundary, proceeding only when both allow. This is a separately-audited control that holds across engines. See Level 2.
And you write no new policy: it stays in the engine your org already authors policy in, audited where the rest of your authorization lives.
Four enforcement points, one engine. The check runs wherever your stack lets you intercept the action: inside an agentic firewall (as a LlamaFirewall scanner or a NeMo Guardrails rail), at the MCP boundary as FastMCP server middleware, or at the agent-to-agent boundary as an A2A executor. Same engine, same fail-closed semantics everywhere; only the boundary differs.
One integration, many policy engines. apparitor speaks the AuthZEN 1.0 interop standard, so the same wiring reaches the engines you already author policy in: OpenFGA (Zanzibar / ReBAC, experimental), Cedar (policy-as-code), and OPA / Rego, with no policy rewrite. OPA and Cedar also have native backends that skip the AuthZEN hop.
Status:
0.1.1, beta. Shipping today: all four enforcement points above and the AuthZEN evaluation pipeline, working end-to-end against any AuthZEN 1.0 PDP (OpenFGA, Cedar, OPA, Cerbos, Topaz) plus native OPA and in-process Cedar backends, with ≥90% test coverage (CI-enforced) on the adapter-free core (seeCHANGELOG). Fail-closed on every error path, subject isolation, and an SSRF-guarded transport are tested invariants. An internal adversarial security review (six findings, all fixed) is documented indocs/security-review.md, and an independent external review is an adoption-gated goal, not yet done. Solo-maintained, best-effort cadence. On the roadmap: a native OpenFGA backend. Seedocs/requirements.mdfor the design andROADMAP. APIs may change.
Install from PyPI:
pip install apparitorEach enforcement point and the in-process Cedar backend are optional extras
([llamafirewall], [nemo], [fastmcp], [a2a], [cedar]). [llamafirewall] pulls a
torch / ML stack; the bare install and every other extra do not. See
docs/setup.md for the full matrix and the per-extra install
commands.
Pick the enforcement point your stack already has. The same AuthorizationEngine runs
behind each one; only the boundary and the identity source differ. Point any of them at an
AuthZEN-compliant policy decision point (PDP) with pdp_url, and resolve a subject per
request (recommended) or set a static agent_id. See
Identity: who the agent acts for.
FastMCP server middleware (pip install "apparitor[fastmcp]"). Gates tools/call,
resources/read, and prompts/get server-side before the tool runs. The subject is the
validated OAuth sub, never a host assertion, so register it after your auth middleware.
Worked proxy example in examples/gateway/.
from fastmcp import FastMCP
from apparitor.fastmcp import FastMCPAuthorizationMiddleware
server = FastMCP("files", auth=...) # auth yields the validated token identity
server.add_middleware(FastMCPAuthorizationMiddleware(pdp_url="https://pdp.internal"))A2A agent executor (pip install "apparitor[a2a]"). Authorizes every agent-to-agent
agent.invoke before the wrapped executor runs; the subject is the server's authenticated
peer.
from a2a.server.request_handlers import DefaultRequestHandler
from apparitor.a2a import A2AAuthorizationExecutor
guarded = A2AAuthorizationExecutor(
MyExecutor(), pdp_url="https://pdp.internal", agent_label="travel-agent"
)
handler = DefaultRequestHandler(agent_executor=guarded, task_store=..., agent_card=...)Inside LlamaFirewall (pip install "apparitor[llamafirewall]"). Bind the scanner to the
assistant role so it gates tool calls before they dispatch. Tool calls in OpenAI, Anthropic,
and LangChain shapes are normalised automatically; an unrecognised shape blocks (fail closed).
from llamafirewall import LlamaFirewall, Role
from apparitor import AuthZENScanner, ScannerConfig
scanner = AuthZENScanner(config=ScannerConfig(pdp_url="https://pdp.internal", agent_id="travel-bot"))
firewall = LlamaFirewall(scanners={Role.ASSISTANT: [scanner]})
result = await firewall.scan_async(assistant_message) # ALLOW / BLOCK / HUMAN_IN_THE_LOOPNeMo Guardrails rail (pip install "apparitor[nemo]"). Registers as a custom action; the
rail refuses denied tool calls, fail-closed under NeMo's mapping. The rail flow lives in your
NeMo config (see the module docstring). Exercised in examples/three-peps/.
from nemoguardrails import LLMRails, RailsConfig
from apparitor.nemo import NeMoAuthorizationRails
rails = LLMRails(RailsConfig.from_path("config"))
NeMoAuthorizationRails(pdp_url="https://pdp.internal").register(rails)Each adapter has more options (list filtering, dual-principal boundaries, per-hook opt-outs)
documented in its module docstring; see docs/setup.md for per-engine wiring.
The AuthZEN client and models are adapter-free and usable on their own:
from apparitor.models import EvaluationRequest needs no firewall dependency.
Every decision needs a subject: the principal your policy is written against. apparitor never infers it from model or tool output (that would be a confused deputy); the host supplies it, request-scoped. There is a maturity ladder of three levels:
- Level 0 — static agent identity. Set
agent_id; every call is authorized as that agent. Enough for policies that don't depend on the end user ("no agent may call a destructive tool"). - Level 1 — the real end user, per request (recommended). Bind the authenticated user
for the agent run with
subject_scope(Subject(...)); it resets on exit, so a subject cannot leak to a later request that reuses the same task/event loop. - Level 2 — the agentic permission boundary (user ∧ agent).
DualPrincipalMapperevaluates the user's grant and the agent's own boundary, proceeding only when both allow, so a jailbroken agent can never borrow its user's rights.
With no resolvable subject the scan fails closed. Enforcement points that carry a
validated identity of their own populate the same seam: the FastMCP middleware reads the
verified OAuth token's sub and it outranks any host-asserted subject. See
docs/setup.md for the full resolution
order, the three levels with code, and dual-principal wiring.
Every path that cannot produce a clean ALLOW refuses: an unreachable or timed-out PDP, a
malformed response (a missing or non-boolean decision is an error, never a coerced
allow), a missing subject, an unparseable tool call. There is no fail-open option; a PDP
failure resolves per on_error to deny (default) or human_review. PDP URLs must be
HTTPS and pass an SSRF guard, TLS verified and redirects never followed (the only opt-out
is the explicit allow_insecure_pdp flag, for local dev). A review_predicate can only
escalate a decision to HUMAN_IN_THE_LOOP, never downgrade one. Decision caching is off
by default and, when enabled, caches ALLOW only, keyed by a digest of the full request
tuple, with a clamped TTL. See docs/requirements.md (§3.6–3.9) for
the full failure-handling and caching design.
Every decision is timed and counted. The scanner (and the standalone AuthorizationEngine)
exposes a metrics sink — by default an in-process InMemoryMetrics with a latency
histogram and decision/cache counters; pass your own MetricsSink to forward to
Prometheus/OpenTelemetry, or NoopMetrics() to disable.
m = scanner.metrics
m.latency_histogram() # [(le_seconds, cumulative_count), …, (+Inf, n)]
m.decisions # {("allow", "success"): 12, ("block", "error"): 1}Each decision also emits one structured audit line (verdict, status, subject id,
correlation id, resource ids, argument fingerprint); raw arguments and tokens are never
logged. The subject id is the decision principal (the OAuth sub under FastMCP, which may
be an email), so treat the apparitor logger as sensitive. The log format is a stability
contract from 0.1.0. For a runnable walk-through — configuring the logger, emitting and
parsing every contract line, and a Prometheus scrape — see
examples/observability/. See docs/audit-log.md
and docs/requirements.md (§3.10).
Enforcement points (the agent-side hooks apparitor plugs into):
| Enforcement point | Vendor | Status |
|---|---|---|
| LlamaFirewall | Meta | shipping (AuthZENScanner) |
| NeMo Guardrails | NVIDIA | shipping (NeMoAuthorizationRails) |
| FastMCP server middleware | Prefect | shipping (FastMCPAuthorizationMiddleware) |
| A2A agent executor | Linux Foundation | shipping (A2AAuthorizationExecutor) |
Policy engines (where the authorization decision is made). apparitor reaches these over
AuthZEN; OPA and Cedar also have native backends that skip the AuthZEN hop, selected by
config (backend="opa" / backend="cedar"):
| Engine | Paradigm | How apparitor reaches it | Example |
|---|---|---|---|
| Mock PDP (testing/demo) | n/a | AuthZEN | examples/mock_pdp/ |
| OpenFGA | Zanzibar / ReBAC | native AuthZEN (experimental) | examples/openfga/ |
| Cedar | policy-as-code (ABAC) | AuthZEN gateway · native in-process (backend="cedar") |
examples/cedar/ |
| OPA / Rego | policy-as-code | AuthZEN gateway · native Data API (backend="opa") |
examples/opa/ |
| Amazon Verified Permissions | managed Cedar | AWS AuthZEN interface | examples/avp/ |
| Any AuthZEN 1.0 PDP (Cerbos, Topaz, …) | varies | AuthZEN | docs/setup.md |