Skip to main content

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.

PyPI Python 3.12+ License: MIT Tests Coverage Status: Beta


                       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

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 is open_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 GuardAsyncGuard:

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 coveropen-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 actor
  • root — check only the originating human
  • all — 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 parityAsyncGuard.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 primitivesTrustGateEvaluator, OperationChainConfig, ActorType.ROBOT / EDGE_DEVICE
v0.8.0 2026-04-19 Consumer integrationGuard.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 integrationGuard.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


License

MIT © 2026 Avinash Singh

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

open_guard_python-0.12.5.tar.gz (668.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

open_guard_python-0.12.5-py3-none-any.whl (173.5 kB view details)

Uploaded Python 3

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

Hashes for open_guard_python-0.12.5.tar.gz
Algorithm Hash digest
SHA256 4f6e4ea1c920d7a5dd372211ef8765cc3d41a3839fe9bc272373dec20737bee8
MD5 40666207236ae71a2f95118c9f8b5da5
BLAKE2b-256 e4b69071d5267d247d7e6456036790b6cb6b8cdd9724660d5bb0bd6adbef38e1

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file open_guard_python-0.12.5-py3-none-any.whl.

File metadata

File hashes

Hashes for open_guard_python-0.12.5-py3-none-any.whl
Algorithm Hash digest
SHA256 7b32bb5149f0ada9d92804ddd8276628cfc505c669483f70bbb92231cb187861
MD5 239f06751be0bb225a8f59e6a38f04f5
BLAKE2b-256 75f43571c282801abc8f04b1efda2c3898a44bf665ce87d8341619187ab1bd81

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page