Vendor-neutral authorization library — unified RBAC, ABAC, TBAC, ReBAC for all actor types
Project description
open-guard
Universal authorization for the AI era. One Python engine for RBAC, ABAC, time, relationships, delegation, and AI-agent identity chains — across humans, services, agents, robots, devices, and conversations.
The same engine answers both questions
───────────────────────────────────────
Traditional SaaS AI-era systems
──────────────── ──────────────
user ──── action ────► resource user
│ asks
▼
"can Alice (editor) write doc-1?" agent ──delegated by user, budget=100, ttl=1h
│ spawns
▼
sub-agent ──trust_score=0.6
│ calls tool
▼
MCP tool ──parameter-level ABAC
│
▼
resource
"Is this *chain* of actors allowed to
perform this action on this resource —
right now, in this tenant, within their
delegated budget, at their trust level,
with the right approvals?"
open-guard answers both questions with the same engine, the same policies, and the same five-model evaluator stack. Use it as a plain Python library, mount its REST surface into a FastAPI app, or run it as a standalone HTTP service that any language can call.
Table of contents
- Why open-guard
- Install
- 30-second quick start
- Every actor is a first-class subject
- Real-world scenarios
- The five authorization models
- AI-era primitives in depth
- Integrations
- Storage backends
- Architecture
- Status
- Release history
- Development
- License
Why open-guard
Existing authorization tools were built for a simpler world: one user, one role, one resource, one hop. That world hasn't gone away — but it's no longer the only world.
A modern stack has humans, services, AI agents, sub-agents, MCP tools, robots, edge devices, and conversation contexts all making and receiving authorization decisions, often inside the same request. Most engines force you to bolt these together yourself, or use a different tool for each.
open-guard is one engine for all of it. The same Guard.authorize(subject, action, resource, tenant) call works for:
- A logged-in user clicking Edit
- A backend service calling another service
- An AI agent acting on behalf of that user, with a 100-call budget and a 1-hour lease
- A sub-agent the first agent spawned, two trust levels removed from the human
- A robot in a warehouse zone, gated by sensor trust score
- An MCP tool call with parameter-level attribute checks
| Concern | Casbin | OPA / Cedar | Auth0 FGA / SpiceDB | open-guard |
|---|---|---|---|---|
| RBAC + ABAC | ✅ | ✅ | ✅ | ✅ |
| ReBAC (Zanzibar relation tuples) | partial | ❌ | ✅ | ✅ |
| Time- and task-scoped (TBAC) | ❌ | ❌ | ❌ | ✅ |
| Trust-score gating (per-actor or chain-wide) | ❌ | ❌ | ❌ | ✅ |
| On-behalf-of identity chains | ❌ | ❌ | ❌ | ✅ recursive, depth-capped |
| Delegation with budgets + leases + cascade | ❌ | ❌ | ❌ | ✅ |
| Tri-state decisions (allow / deny / pending approval) | ❌ | ❌ | ❌ | ✅ |
| Action-pattern → evaluator routing | ❌ | ❌ | ❌ | ✅ OperationChain |
| MCP tool-call adapter | ❌ | ❌ | ❌ | ✅ |
| Prompt-injection defense (taint marking) | ❌ | ❌ | ❌ | ✅ |
| Vendor-neutral (run anywhere, no SaaS lock-in) | ✅ | ✅ | partial | ✅ |
| Multi-tenant by construction | manual | manual | ✅ | ✅ every call takes tenant_id |
| Native async (non-blocking I/O) | ❌ | ❌ | ✅ | ✅ AsyncGuard |
The goal isn't to replace any of those tools where they fit. It's to give you one mental model that scales from a single-tenant CRUD app to a multi-tenant multi-agent platform without swapping engines.
Install
pip install open-guard-python
Bare install gives you the in-memory engine and all five evaluators. Optional extras:
| Extra | Adds | Install |
|---|---|---|
sql |
SQLAlchemy stores (SQLite, PostgreSQL) | pip install open-guard-python[sql] |
sql-async |
Async SQLAlchemy + aiosqlite |
pip install open-guard-python[sql-async] |
fastapi |
AdminRouter for mounting into FastAPI |
pip install open-guard-python[fastapi] |
service |
Standalone HTTP service (FastAPI + uvicorn) | pip install open-guard-python[service] |
client |
Python SDK for the HTTP service | pip install open-guard-python[client] |
mcp |
MCP tool-call adapter | pip install open-guard-python[mcp] |
cli |
open-guard CLI |
pip install open-guard-python[cli] |
The PyPI package is
open-guard-python. The import path isopen_guard.
30-second quick start
from open_guard import Guard, Subject, Action, Resource, Policy, PolicyEffect
# One-line factory wires every store and every evaluator
guard = Guard.from_config(backend="memory")
# "Editors can read or write documents"
guard.policy_store.add(Policy(
tenant_id="acme",
evaluator_type="rbac",
effect=PolicyEffect.ALLOW,
subject_match={"roles": ["editor"]},
action_match=["read", "write"],
))
decision = guard.authorize(
subject=Subject(id="user-1", attributes={"roles": ["editor"]}),
action=Action(name="write"),
resource=Resource(id="doc-1", resource_type="document"),
tenant_id="acme",
)
print(decision.allowed) # True
print(decision.reason) # Human-readable explanation
Async? Swap Guard → AsyncGuard:
from open_guard import AsyncGuard
guard = await AsyncGuard.from_config(backend="memory")
decision = await guard.authorize(subject, action, resource, tenant_id="acme")
That's the entire happy path. Everything below is what open-guard adds when one role + one action isn't enough.
Every actor is a first-class subject
Most authz libraries assume subject = user. open-guard treats every actor type as first-class — with the same API, same policies, and same evaluators. Mix and match in the same chain.
┌──────────────────┐
│ Subject │
│ (any actor) │
└────────┬─────────┘
│
┌─────────┬────────────┬───────┴────────┬──────────────┬────────────┐
▼ ▼ ▼ ▼ ▼ ▼
USER AGENT SERVICE DEVICE ROBOT EDGE_DEVICE CONVERSATION
(humans) (AI agents) (M2M, micro- (IoT sensors) (physical (gateways, (chat-thread
services) robots, fog nodes) context)
drones)
attributes: roles, trust_score, region, zone, ...
on_behalf_of: another Subject (recursive — builds the chain)
from open_guard import Subject, ActorType
Subject(id="user-1", actor_type=ActorType.USER, attributes={"roles": ["admin"]})
Subject(id="agent-42", actor_type=ActorType.AGENT, attributes={"trust_score": 0.85})
Subject(id="svc-billing", actor_type=ActorType.SERVICE)
Subject(id="sensor-5", actor_type=ActorType.DEVICE, attributes={"zone": "warehouse"})
Subject(id="robot-arm-3", actor_type=ActorType.ROBOT, attributes={"trust_score": 0.95})
Subject(id="edge-gw-7", actor_type=ActorType.EDGE_DEVICE, attributes={"region": "us-west"})
Subject(id="conv-123", actor_type=ActorType.CONVERSATION)
Any subject can act on_behalf_of any other subject. The chain is recursive (depth-capped, default 5). Policies can reason over the whole chain — see the on-behalf-of section below.
Real-world scenarios
Four scenarios, one library. Each shows a meaningfully different shape of authorization problem.
Scenario 1 — Multi-tenant SaaS (the traditional case)
A document app where every tenant has its own users, roles, and resources. Standard RBAC + a little ABAC for classification.
from open_guard import Guard, Subject, Action, Resource, Policy, PolicyEffect
guard = Guard.from_config("postgresql://...")
# Tenant acme: editors can read+write any document
guard.policy_store.add(Policy(
tenant_id="acme", evaluator_type="rbac",
subject_match={"roles": ["editor"]}, action_match=["read", "write"],
effect=PolicyEffect.ALLOW,
))
# Tenant acme: confidential docs only readable by users in legal/compliance
guard.policy_store.add(Policy(
tenant_id="acme", evaluator_type="abac",
action_match="read",
conditions={
"resource.classification": {"op": "eq", "value": "confidential"},
"subject.department": {"op": "in", "value": ["legal", "compliance"]},
},
effect=PolicyEffect.ALLOW,
))
guard.authorize(
subject=Subject(id="alice", attributes={"roles": ["editor"], "department": "legal"}),
action=Action(name="read"),
resource=Resource(id="doc-99", resource_type="document",
attributes={"classification": "confidential"}),
tenant_id="acme",
)
No AI, no chains, no agents. Just clean RBAC + ABAC with tenant isolation built in. This is exactly what Casbin / Cedar / Auth0 cover — open-guard covers it equally well, and you've already paid for the AI capabilities you'll need next year.
Scenario 2 — AI assistant with budget and on-behalf-of
A user asks an assistant to book travel. The assistant has a 50-API-call budget for the next hour, can act on the user's behalf, but only for travel-related actions.
┌────────┐ ┌──────────────────┐
│ user │ "book me a flight" │ travel:search │
│ │ ─────────────────────────────────────────► │ travel:book │
└────────┘ │ travel:cancel │
│ └──────────────────┘
│ delegates ▲
│ scope = travel:* │
│ max_uses = 50 │
│ lease_ttl = 1h │
▼ │
┌────────────┐ authorize_and_consume(action, resource) │
│ assistant │ ──────────────────────────────────────────────► │
│ (agent) │ ← decision.allowed + uses_remaining decremented
└────────────┘
from open_guard import Guard, Subject, Action, Resource, DelegationScope, ActorType
from datetime import timedelta
guard = Guard.from_config(backend="memory")
user = Subject(id="user-1", actor_type=ActorType.USER)
assistant = Subject(id="assistant-1", actor_type=ActorType.AGENT, on_behalf_of=user)
# User delegates a bounded scope to the assistant
guard.delegate(
grantor=user, grantee=assistant,
scope=DelegationScope(action="travel:*", resource_type="*"),
max_uses=50,
lease_ttl=timedelta(hours=1),
tenant_id="acme",
)
# Every action call consumes from the budget; CAS-safe under concurrency
decision = guard.authorize_and_consume(
subject=assistant,
action=Action(name="travel:book"),
resource=Resource(id="flight-AA123", resource_type="flight"),
tenant_id="acme",
)
# decision.allowed → True, budget drops to 49
# decision.attributed_delegation_ids → which delegation paid for it
If the assistant tries payment:charge, the delegation scope doesn't match → decision denied. If it tries to delegate further to a sub-agent, you choose whether that's allowed via Policy.non_delegable or the default SelfDelegateOnly meta-policy. If the lease expires (1 hour passes without a renew() call), every subsequent action is denied — without you writing a single cron job.
Scenario 3 — Multi-agent research with trust gating
A research orchestrator spawns sub-agents that crawl external sources. Each sub-agent has a computed trust_score; the deeper into the chain you go, the lower the trust. A sensitive write action requires every actor in the chain to clear a 0.7 threshold.
┌──────────────────┐
│ user (trust 1.0) │
└────────┬─────────┘
│ on_behalf_of
▼
┌────────────────────────────┐
│ orchestrator (trust 0.9) │
└────────┬───────────────────┘
│ on_behalf_of
▼
┌────────────────────────────┐ policy.conditions:
│ crawler-agent (trust 0.6) │ ──────► { "min_trust_score": 0.7,
└────────────────────────────┘ "trust_scope": "all" }
chain check:
user (1.0) ✓
orchestrator (0.9) ✓
crawler-agent (0.6) ✗
→ DENIED
from open_guard import Subject, Policy, PolicyEffect, ActorType
user = Subject(id="user-1", actor_type=ActorType.USER,
attributes={"trust_score": 1.0})
orchestrator = Subject(id="orchestrator-1", actor_type=ActorType.AGENT,
attributes={"trust_score": 0.9}, on_behalf_of=user)
crawler = Subject(id="crawler-1", actor_type=ActorType.AGENT,
attributes={"trust_score": 0.6}, on_behalf_of=orchestrator)
# Policy: writing requires the entire chain to clear 0.7
guard.policy_store.add(Policy(
tenant_id="research", evaluator_type="trust",
action_match="kb:write",
conditions={"min_trust_score": 0.7, "trust_scope": "all"},
effect=PolicyEffect.ALLOW,
))
guard.authorize(crawler, Action(name="kb:write"), Resource(id="doc", resource_type="kb"),
tenant_id="research")
# .allowed = False — crawler at 0.6 fails the gate, even though user is fully trusted
Switch trust_scope to immediate to check only the requesting actor, or root to check only the originating human. Same policy, three different stances on chain trust.
Scenario 4 — Robot fleet with delegated, time-bounded authority
A maintenance dispatcher delegates a set of motion commands to a specific robot for a 30-minute window, with a hard cap of 200 commands and the right to cascade-revoke if anything looks wrong.
from open_guard import ActorType, Subject, DelegationScope
from datetime import timedelta
dispatcher = Subject(id="dispatcher-1", actor_type=ActorType.SERVICE)
robot = Subject(id="robot-arm-3", actor_type=ActorType.ROBOT, on_behalf_of=dispatcher,
attributes={"zone": "warehouse-a", "trust_score": 0.95})
delegation_id = guard.delegate(
grantor=dispatcher, grantee=robot,
scope=DelegationScope(action="motion:*", resource_type="cell"),
max_uses=200,
lease_ttl=timedelta(minutes=30),
tenant_id="factory",
).id
# Robot operates against any cell within its delegation
guard.authorize_and_consume(robot, Action(name="motion:pick"),
Resource(id="cell-12", resource_type="cell",
attributes={"zone": "warehouse-a"}),
tenant_id="factory")
# Operator sees an anomaly — revoke immediately, including any sub-delegations
guard.revoke_delegation(delegation_id, tenant_id="factory")
Optional ABAC overlay: subject.zone == resource.zone to refuse motion in cells the robot isn't authorized for, regardless of delegation scope.
The five authorization models
open-guard ships five composable evaluators. You can use one, combine many, or route each action to a different combination via OperationChain.
RBAC — Role-Based
Role matching with optional resource scoping, hierarchy, priority-based conflict resolution, NIST-style session-aware role subset activation.
Policy(evaluator_type="rbac",
subject_match={"roles": ["editor"]},
action_match="write")
ABAC — Attribute-Based
Condition evaluation over subject, resource, action, and context attributes. 14 operators (eq, in, regex, contains_any, is_subset, …), OR logic via condition_groups, chain-aware predicates (chain.root_actor.*), session namespace (session.scope), trusted_only modifier for prompt-injection defense.
Policy(evaluator_type="abac", conditions={
"subject.department": {"op": "eq", "value": "engineering"},
"resource.classification": {"op": "in", "value": ["public", "internal"]},
})
TBAC — Time- and Task-Based
Absolute time windows, recurring schedules (IANA timezone, DST-aware), ISO-8601 TTLs, task-scoped permissions with lifecycle state machine, parent-child hierarchy that narrows permissions down the tree.
Policy(evaluator_type="tbac", conditions={
"time_window": {"not_before": "2026-04-16T00:00:00Z",
"not_after": "2026-04-17T00:00:00Z"},
"task_id": "task-123",
})
ReBAC — Relationship-Based
Google Zanzibar-style relation tuples with recursive graph traversal, computed permissions (union, intersection, exclusion, arrow), wildcards, subject sets, memoization, max-depth cycle prevention.
Policy(evaluator_type="rebac", conditions={
"object_type": "document",
"permission": "can_view",
"subject_type": "user",
})
Trust — Trust-Score Gating
Chain-aware trust evaluation built for agent systems. Reads subject.attributes["trust_score"]. Three scopes:
immediate(default) — check only the requesting actorroot— check only the originating humanall— every actor in the chain must clear the threshold
Policy(evaluator_type="trust", conditions={
"min_trust_score": 0.7,
"trust_scope": "all",
})
Compose them however you want
from open_guard import Guard, OperationChainConfig, OperationRule
guard = Guard.from_config(backend="memory", operation_chain=OperationChainConfig(rules=[
# Writes need everything
OperationRule(action_pattern="knowledge:write",
evaluators=["rbac", "abac", "trust"]),
# Reads are cheap
OperationRule(action_pattern="knowledge:read",
evaluators=["rbac", "abac"]),
# Admin actions: strict RBAC only, no soft signals
OperationRule(action_pattern="admin:*",
evaluators=["rbac"]),
]))
AI-era primitives in depth
This is what open-guard adds on top of "five evaluators." Each section is a building block built specifically for agent / robot / multi-actor systems.
1. On-behalf-of identity chains
Authorization in agent systems is rarely a single hop. Subject.on_behalf_of lets you model the whole chain; policies can reason over it via ABAC predicates (chain.depth, chain.root_actor.*, chain.ancestors, any_matches).
sequenceDiagram
participant U as User<br/>(trust=1.0, roles=[researcher])
participant A as Agent<br/>(trust=0.8)
participant S as Sub-agent<br/>(trust=0.6)
participant G as Guard
U->>A: "summarize last quarter's reports"
A->>S: spawn sub-agent
S->>G: authorize(subject=sub_agent, action="kb:read", resource=...)
Note over G: chain = [sub_agent, agent, user]<br/>chain.depth = 3<br/>chain.root_actor = user<br/>min trust in chain = 0.6
G-->>S: decision based on chain-wide policy
user = Subject(id="user-1", actor_type=ActorType.USER,
attributes={"roles": ["researcher"], "trust_score": 1.0})
agent = Subject(id="agent-1", actor_type=ActorType.AGENT,
attributes={"trust_score": 0.8}, on_behalf_of=user)
sub_agent = Subject(id="sub-1", actor_type=ActorType.AGENT,
attributes={"trust_score": 0.6}, on_behalf_of=agent)
# ABAC condition over the chain
Policy(evaluator_type="abac", conditions={
"chain.depth": {"op": "lte", "value": 3},
"chain.root_actor.roles": {"op": "contains", "value": "researcher"},
"chain.ancestors": {"op": "any_matches", "value": {
"actor_type": {"op": "eq", "value": "USER"},
}},
})
Chain depth is enforced both at construction and at runtime (default cap 5, configurable). Trying to construct or evaluate a chain past the cap raises ChainDepthExceededError.
2. Delegation with budgets, leases, and cascade revocation
Agents should not have inherent permissions. They receive bounded, time-limited, revocable delegation from a grantor (usually a human).
flowchart LR
U[user / grantor] -- "delegate( scope, max_uses=100, lease_ttl=1h )" --> D1[delegation-1<br/>uses=100, expires in 1h]
D1 -- "delegate further" --> D2[sub-delegation-2<br/>narrower scope]
D1 -- "delegate further" --> D3[sub-delegation-3<br/>narrower scope]
A[agent / grantee] -- "authorize_and_consume()" --> D1
A -- "CAS decrement uses" --> D1
R[user calls<br/>revoke_delegation D1] --> D1
D1 -. cascade .-> D2
D1 -. cascade .-> D3
from datetime import timedelta
# Push delegation
delegation = guard.delegate(
grantor=user, grantee=agent,
scope=DelegationScope(action="read", resource_type="document"),
max_uses=100, # Budget
lease_ttl=timedelta(hours=1), # Auto-expire
tenant_id="acme",
)
# Each call atomically consumes from the budget
guard.authorize_and_consume(agent, action, resource, tenant_id="acme")
# Renewal extends the lease (only if grantor or grantee permits via `renewable_by`)
guard.renew_delegation(delegation.id, additional_ttl=timedelta(minutes=30),
tenant_id="acme")
# Revoke — every sub-delegation that descended from this one also revokes
guard.revoke_delegation(delegation.id, tenant_id="acme")
CAS-safe under concurrency (RLock in-memory, UPDATE ... WHERE in SQL). When uses_remaining hits 0 the delegation auto-transitions to EXHAUSTED. When the lease expires it auto-transitions to EXPIRED. Policy.non_delegable and the default SelfDelegateOnly meta-policy give you knobs over who can delegate to whom.
3. Trust scoring (chain-aware)
TrustGateEvaluator is its own evaluator type because trust deserves first-class semantics distinct from ABAC.
trust_scope="immediate" → check just the requesting actor
trust_scope="root" → check just the originating human
trust_scope="all" → check every actor in the chain
Useful for: gating sensitive actions to high-trust agents, refusing destructive actions when any chain member is below threshold, requiring human (root) at the top regardless of how trusted the agent is.
4. Action-pattern routing (OperationChain)
Different actions deserve different evaluator combinations. OperationChainConfig routes each action pattern to its evaluator stack (fnmatch glob, first-match-wins).
OperationChainConfig(rules=[
OperationRule(action_pattern="*:delete", evaluators=["rbac", "abac", "trust"]),
OperationRule(action_pattern="*:read", evaluators=["rbac", "abac"]),
OperationRule(action_pattern="tool:*", evaluators=["rbac", "trust"]),
OperationRule(action_pattern="admin:*", evaluators=["rbac"]),
])
5. Tri-state decisions and async approval
Some actions should pause and wait for a human before proceeding. open-guard returns DecisionStatus.PENDING_APPROVAL, gives you metadata for whatever channel you use (Slack, email, webhook), and waits for an approve() / reject() call to resolve.
stateDiagram-v2
[*] --> Evaluating
Evaluating --> Allowed: all checks pass, no approval required
Evaluating --> Denied: any check fails
Evaluating --> PendingApproval: policy requires_approval && checks pass
PendingApproval --> Approved: quorum met within TTL
PendingApproval --> Rejected: any required approver says no
PendingApproval --> ExpiredApproval: TTL elapsed without quorum
Approved --> [*]
Rejected --> [*]
Denied --> [*]
ExpiredApproval --> [*]
from open_guard import Policy, ApprovalRequirement, DecisionStatus
from datetime import timedelta
Policy(
evaluator_type="rbac", action_match="data:delete",
requires_approval=ApprovalRequirement(
approvers=["role:manager"], quorum="any", ttl=timedelta(hours=4),
),
)
decision = guard.authorize(agent, Action(name="data:delete"), resource, tenant_id="acme")
if decision.status == DecisionStatus.PENDING_APPROVAL:
# Dispatch via your channel — open-guard never sends messages itself
notify_managers(decision.approval_request_id)
# Later, when the manager approves:
guard.approve(decision.approval_request_id, tenant_id="acme", approver="manager-1")
SHA-256 idempotency on (tenant, subject.id, action, resource.id, resource_type) so the same logical request maps to the same approval request even if dispatched multiple times.
6. Reverse lookup — "what can this subject do?"
For RAG filtering, UI menus, and policy debugging. Composes RBAC + ABAC + TBAC + ReBAC across all stores with cursor pagination, deny precedence, chain enforcement, and pending-approval exclusion.
result = guard.list_authorized(
subject=user, action=Action(name="read"), resource_type="document",
tenant_id="acme", limit=100,
)
# result.resource_ids → list of IDs the subject is currently allowed to read
# result.next_cursor → opaque pagination token
Sync and async parity (AsyncGuard.list_authorized ships the same API).
7. Prompt-injection defense
Two complementary layers, both on by default:
┌──────────────────────────────────────────────────────────────────┐
│ ReadOnlyPolicyView wraps PolicyStorePort inside PolicyRouter │
│ → no evaluator can mutate policy state mid-evaluation │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Subject.attributes = {"role": "extracted_from_user_content", │
│ "_trusted": {"role": False}} │
│ │
│ → ABAC policies with `trusted_only=True` refuse to evaluate │
│ on the tainted attribute, even if the value looks correct │
└──────────────────────────────────────────────────────────────────┘
When you extract attributes from user-controlled content (LLM output, document parsing, web scraping), flag them as untrusted. Sensitive policies opt in to trusted_only=True and refuse to be fooled.
8. MCP tool-call adapter
Native Model Context Protocol adapter. Authorize tool calls in agent systems with parameter-level ABAC, chain propagation, pre-authorization schema hygiene, and pending-approval surface for dangerous tools.
sequenceDiagram
participant Agent as AI Agent
participant MCP as MCPGuard
participant R as Session Resolver
participant G as Guard
Agent->>MCP: check_tool_call("search", session_id, params)
MCP->>R: resolve(session_id) → Subject + chain
MCP->>G: authorize(subject, action="tool:search", resource=..., context={params: ...})
Note over G: ABAC reads params via dot-paths<br/>e.g. params.query, params.limit
G-->>MCP: Decision
MCP-->>Agent: allow / deny / pending_approval
opt If pending
MCP->>MCP: surface approval metadata
end
from open_guard.adapters.mcp import MCPGuard
mcp = MCPGuard(guard=guard, resolver=session_resolver, default_tenant_id="acme")
result = mcp.check_tool_call(session_id="s1", tool_name="search",
params={"query": "...", "limit": 100})
tools = mcp.list_authorized_tools(session_id="s1")
SDK-agnostic core; the optional [mcp] extra adds direct duck-typed binding to the MCP Python SDK.
9. Explainable decisions
Every authorize call has a traced variant. The trace is JSON-serializable — ship it to logs, a SIEM, or feed it back to an agent for reasoning.
decision, trace = guard.authorize_traced(subject, action, resource, tenant_id="acme")
# trace.evaluator_results → which evaluators ran, which policies matched / rejected
# trace.actor_chain → full OBO chain snapshot
# trace.composition → "all-allow" | "deny-wins" | "pending-approval"
# trace.duration_ms → total evaluation time
10. ABAC condition operators (reference)
| Operator | Example | Description |
|---|---|---|
eq / neq |
{"op": "eq", "value": "engineering"} |
Equals / not equals |
in / not_in |
{"op": "in", "value": ["a", "b"]} |
In / not in a list |
contains / not_contains |
{"op": "contains", "value": "ml"} |
List or string contains |
gt / gte / lt / lte |
{"op": "gte", "value": 9} |
Numeric comparison |
regex / not_regex |
{"op": "regex", "value": ".*@co\\.com$"} |
Regex match |
exists |
{"op": "exists", "value": true} |
Attribute exists |
contains_any |
{"op": "contains_any", "value": ["a", "b"]} |
Any element overlaps |
contains_all |
{"op": "contains_all", "value": ["a", "b"]} |
All elements present |
is_subset |
{"op": "is_subset", "value": ["a", "b", "c"]} |
Left subset of right |
any_matches |
{"op": "any_matches", "value": {...}} |
Chain ancestor sub-condition |
Integrations
MCP — Model Context Protocol
pip install open-guard-python[mcp]
See §8 above.
FastAPI
pip install open-guard-python[fastapi]
Mount the admin REST surface:
from fastapi import FastAPI
from open_guard import PolicyAdmin, AdminRouter
app = FastAPI()
admin = PolicyAdmin(guard)
app.include_router(AdminRouter(admin=admin), prefix="/admin")
Or wire decision endpoints with middleware:
from fastapi import Depends
from open_guard import ActorType
from open_guard.api.fastapi import OpenGuardMiddleware, RequirePermission, RequireActor
app.add_middleware(OpenGuardMiddleware, guard=guard)
@app.get("/docs/{doc_id}")
def get_doc(doc_id: str, _auth=Depends(RequirePermission("read", "document"))):
...
@app.get("/human-only")
def human_only(_auth=Depends(RequireActor(ActorType.USER))):
...
Standalone HTTP service
Run open-guard as a service that any language can call:
pip install open-guard-python[service]
uvicorn open_guard.service.app:create_app --factory
Exposes /authorize, /authorize/traced, /list_authorized, /authorize_and_consume, and the full /admin/* CRUD. Pluggable auth: built-in ApiKeyAuth (sha-256 hashed keys), ShieldAuth (open-shield JWT, optional [shield] extra), or CallableAuth (BYO).
Python SDK
pip install open-guard-python[client]
from open_guard.client import OpenGuardClient, ApiKey
client = OpenGuardClient(base_url="https://og.example.com", auth=ApiKey("og_..."))
decision = client.decisions.authorize(subject={...}, action={...}, resource={...})
client.admin.policies.create(tenant_id="acme", evaluator_type="rbac", ...)
Sync and async clients, namespaced API (client.decisions.*, client.admin.{policies,roles,relationships,delegations,sessions}.*), idempotent-only retry by default.
Admin SDK (in-process)
Same admin surface as the REST API, without the HTTP hop:
from open_guard import PolicyAdmin
admin = PolicyAdmin(guard)
admin.create_policy(tenant_id="acme", evaluator_type="rbac",
subject_match={"roles": ["viewer"]}, action_match="read")
admin.assign_role(tenant_id="acme", subject_id="user-1", role="viewer")
admin.list_policies(tenant_id="acme")
Storage backends
guard = Guard.from_config(backend="memory") # in-memory
guard = Guard.from_config("sqlite:///local.db") # SQLite, file-based
guard = Guard.from_config("postgresql://user:pass@host/db") # PostgreSQL
guard = await AsyncGuard.from_config(backend="memory")
guard = await AsyncGuard.from_config("sqlite+aiosqlite:///local.db")
guard = await AsyncGuard.from_config("postgresql+asyncpg://user:pass@host/db")
DB tooling: create_all_tables(engine), get_ddl(dialect), CLI (open-guard db init, open-guard db schema).
Multi-tenancy is structural. Every policy carries a tenant_id; every authorize call takes one. Cross-tenant access is impossible by construction, not by convention.
Architecture
Clean / hexagonal. The domain is pure Python with zero external dependencies. Adapters are swappable. Adding a new authorization model is "implement EvaluatorPort and register it."
┌─────────────────────────────────────────────────────────────────────────┐
│ API Layer │
│ ───────── │
│ • FastAPI middleware + admin router (open_guard.api.fastapi) │
│ • MCP tool-call adapter (open_guard.adapters.mcp) │
│ • Standalone HTTP service (open_guard.service) │
│ • Python SDK (sync + async) (open_guard.client) │
└────────────────────────────┬────────────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────────────┐
│ Domain Layer (pure Python — no external deps) │
│ ──────────── │
│ Entities: Subject, Action, Resource, Policy, Decision, Delegation, │
│ Session, Task, ApprovalRequest, RelationTuple, ... │
│ │
│ Services: Guard → orchestrates policy router + evaluators │
│ AsyncGuard → async-native parity │
│ PolicyRouter → applies ReadOnlyPolicyView, fans out │
│ DelegationManager → push/pull, budgets, leases, cascades │
│ SessionManager → multi-session per subject, NIST RBAC │
│ ApprovalManager → quorum, TTL, idempotency │
│ TaskManager → task lifecycle + parent-child narrowing │
│ │
│ Ports: EvaluatorPort, PolicyStorePort, RelationshipStorePort, │
│ DelegationStorePort, SessionStorePort, ApprovalStorePort, │
│ TaskStorePort, CandidateResourcePort, RevocationStreamPort │
│ (every port has an async counterpart) │
└────────────────────────────┬────────────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────────────┐
│ Adapter Layer (swappable implementations) │
│ ───────────── │
│ Evaluators: RBAC, ABAC, TBAC, ReBAC, TrustGate (+ async ReBAC) │
│ Stores: InMemory* (7 store types, sync + async) │
│ SQLAlchemy* (7 store types, sync + async via asyncpg / │
│ aiosqlite) │
└─────────────────────────────────────────────────────────────────────────┘
Status
Beta. Twelve releases, 1235 tests pass at 93% coverage, API stable since v0.4.0, in real-world use inside the Cerebrio platform. Appropriate for production workloads where you can pin versions and follow the changelog. The 1.0 stamp is reserved for after an external security review and a documented breaking-change policy.
Semver: patch (0.12.x) is bug-fix-only and never breaks API. Minor (0.x.0) may add new ports, evaluators, or extras but avoids breaking existing public APIs. Anything breaking is called out in the changelog.
Async parity: Guard and AsyncGuard are at full feature parity as of v0.12.0 (delegation, approval, list_authorized, sessions, all five evaluators).
Service Mode wire contract: SERVICE_API_VERSION = "v1", stable. Breaking wire changes require a bump.
Release history
| Version | Date | Highlights |
|---|---|---|
| v0.12.5 | 2026-05-25 | README rewrite: universal-authz positioning, diagrams, real-world scenarios |
| v0.12.4 | 2026-05-25 | First release under the avinash-singh-io PyPI account; Trusted Publisher (OIDC) auto-publish workflow |
| v0.12.3 | 2026-05-25 | Account migration recovery (ownership transferred from prior account) |
| v0.12.0–v0.12.2 | 2026-05-25 | Initial uploads went to the wrong PyPI account and were deleted; versions permanently burned per PyPI policy. v0.12.3+ contains the same engine. |
| v0.12.0 (logical) | 2026-05-18 | Async parity — AsyncGuard.list_authorized, AsyncDelegationEvaluator, AsyncApprovalManager |
| v0.11.0 | 2026-05-04 | Service Mode + Python SDK — standalone HTTP service, sync + async Python clients |
| v0.10.0 | 2026-04-28 | Production integration — async SQL backend, resource-scoped RBAC, check_relationship |
| v0.9.0 | 2026-04-20 | AI-era primitives — TrustGateEvaluator, OperationChainConfig, ActorType.ROBOT / EDGE_DEVICE |
| v0.8.0 | 2026-04-19 | Consumer integration — Guard.from_config(), PolicyAdmin, AdminRouter, AsyncGuard, CLI |
| v0.7.0 | 2026-04-19 | Production hardening — sessions, policy isolation, push revocation |
| v0.6.0 | 2026-04-19 | Delegation & bounds — push/pull, usage caps, leases, cascade revocation |
| v0.5.0 | 2026-04-18 | Agent integration — Guard.list_authorized, MCP adapter |
| v0.4.0 | 2026-04-18 | Agentic core — on-behalf-of chains, tri-state decisions, decision traces |
| v0.3.0 | 2026-04-17 | ReBAC + SQLAlchemy — Zanzibar relation tuples, SQL stores |
| v0.2.0 | 2026-04-16 | TBAC — time windows, recurring schedules, task-scoped permissions |
| v0.1.0 | 2026-04-16 | RBAC + ABAC core |
Next: gRPC wrapper + TypeScript / Go SDKs (Phase 12). No date — lands when a real non-Python consumer needs it.
Ecosystem
open-guard is standalone, but it pairs cleanly with the Cerebrio identity stack:
Identity Provider (Logto, Keycloak, Auth0, Cognito, ...)
│
▼
open-shield-python ─── authentication ── "who are you?"
│
▼
open-guard ─── authorization ── "can you do this?"
│
▼
Your application ─── humans, agents, robots, services, conversations
Development
git clone https://github.com/avinash-singh-io/open-guard.git
cd open-guard
uv sync # Install all dev dependencies
uv run pytest # 1235 tests, 93% coverage
uv run mypy src/ --strict # Strict type check
uv run ruff check . # Lint
Pull requests welcome. The repo follows conventional commits and atomic-commit hygiene; see CLAUDE.md for the project's working rules.
Links
- PyPI: https://pypi.org/project/open-guard-python/
- Source: https://github.com/avinash-singh-io/open-guard
- Issues: https://github.com/avinash-singh-io/open-guard/issues
License
MIT © 2026 Avinash Singh
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file open_guard_python-0.12.5.tar.gz.
File metadata
- Download URL: open_guard_python-0.12.5.tar.gz
- Upload date:
- Size: 668.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f6e4ea1c920d7a5dd372211ef8765cc3d41a3839fe9bc272373dec20737bee8
|
|
| MD5 |
40666207236ae71a2f95118c9f8b5da5
|
|
| BLAKE2b-256 |
e4b69071d5267d247d7e6456036790b6cb6b8cdd9724660d5bb0bd6adbef38e1
|
Provenance
The following attestation bundles were made for open_guard_python-0.12.5.tar.gz:
Publisher:
publish.yml on avinash-singh-io/open-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
open_guard_python-0.12.5.tar.gz -
Subject digest:
4f6e4ea1c920d7a5dd372211ef8765cc3d41a3839fe9bc272373dec20737bee8 - Sigstore transparency entry: 1626322245
- Sigstore integration time:
-
Permalink:
avinash-singh-io/open-guard@4f7bcdba70046cad00fb23b0e0ad32cc4ae10276 -
Branch / Tag:
refs/tags/v0.12.5 - Owner: https://github.com/avinash-singh-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4f7bcdba70046cad00fb23b0e0ad32cc4ae10276 -
Trigger Event:
push
-
Statement type:
File details
Details for the file open_guard_python-0.12.5-py3-none-any.whl.
File metadata
- Download URL: open_guard_python-0.12.5-py3-none-any.whl
- Upload date:
- Size: 173.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b32bb5149f0ada9d92804ddd8276628cfc505c669483f70bbb92231cb187861
|
|
| MD5 |
239f06751be0bb225a8f59e6a38f04f5
|
|
| BLAKE2b-256 |
75f43571c282801abc8f04b1efda2c3898a44bf665ce87d8341619187ab1bd81
|
Provenance
The following attestation bundles were made for open_guard_python-0.12.5-py3-none-any.whl:
Publisher:
publish.yml on avinash-singh-io/open-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
open_guard_python-0.12.5-py3-none-any.whl -
Subject digest:
7b32bb5149f0ada9d92804ddd8276628cfc505c669483f70bbb92231cb187861 - Sigstore transparency entry: 1626322330
- Sigstore integration time:
-
Permalink:
avinash-singh-io/open-guard@4f7bcdba70046cad00fb23b0e0ad32cc4ae10276 -
Branch / Tag:
refs/tags/v0.12.5 - Owner: https://github.com/avinash-singh-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4f7bcdba70046cad00fb23b0e0ad32cc4ae10276 -
Trigger Event:
push
-
Statement type: