Metadata-Version: 2.4
Name: open-guard-python
Version: 0.12.5
Summary: Vendor-neutral authorization library — unified RBAC, ABAC, TBAC, ReBAC for all actor types
Project-URL: Homepage, https://github.com/avinash-singh-io/open-guard
Project-URL: Repository, https://github.com/avinash-singh-io/open-guard
Project-URL: Bug Tracker, https://github.com/avinash-singh-io/open-guard/issues
Author-email: Avinash Singh <avinashsingh539+og@gmail.com>
License: MIT
License-File: LICENSE
Keywords: abac,access-control,ai-agents,authorization,policy,rbac,rebac,security,tbac
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: pydantic-settings>=2.0.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: cli
Requires-Dist: click>=8.0; extra == 'cli'
Provides-Extra: client
Requires-Dist: httpx>=0.27.0; extra == 'client'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.129.0; extra == 'fastapi'
Provides-Extra: mcp
Requires-Dist: mcp>=0.3; extra == 'mcp'
Provides-Extra: service
Requires-Dist: fastapi>=0.129.0; extra == 'service'
Requires-Dist: uvicorn[standard]>=0.40.0; extra == 'service'
Provides-Extra: shield
Requires-Dist: open-shield-python>=0.1.0; extra == 'shield'
Provides-Extra: sql
Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
Provides-Extra: sql-async
Requires-Dist: aiosqlite>=0.20.0; extra == 'sql-async'
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'sql-async'
Description-Content-Type: text/markdown

# 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](https://img.shields.io/pypi/v/open-guard-python.svg)](https://pypi.org/project/open-guard-python/)
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-1235%20passing-brightgreen.svg)](#development)
[![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen.svg)](#development)
[![Status: Beta](https://img.shields.io/badge/status-beta-yellow.svg)](#status)

---

```
                       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](#why-open-guard)
- [Install](#install)
- [30-second quick start](#30-second-quick-start)
- [Every actor is a first-class subject](#every-actor-is-a-first-class-subject)
- [Real-world scenarios](#real-world-scenarios)
- [The five authorization models](#the-five-authorization-models)
- [AI-era primitives in depth](#ai-era-primitives-in-depth)
- [Integrations](#integrations)
- [Storage backends](#storage-backends)
- [Architecture](#architecture)
- [Status](#status)
- [Release history](#release-history)
- [Development](#development)
- [License](#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

```bash
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

```python
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`:

```python
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)
```

```python
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](#1-on-behalf-of-identity-chains) 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.

```python
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
   └────────────┘
```

```python
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
```

```python
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.

```python
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`](#4-action-pattern-routing-operationchain).

### RBAC — Role-Based

Role matching with optional resource scoping, hierarchy, priority-based conflict resolution, NIST-style session-aware role subset activation.

```python
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.

```python
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.

```python
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.

```python
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

```python
Policy(evaluator_type="trust", conditions={
    "min_trust_score": 0.7,
    "trust_scope": "all",
})
```

### Compose them however you want

```python
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`).

```mermaid
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
```

```python
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).

```mermaid
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
```

```python
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).

```python
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.

```mermaid
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 --> [*]
```

```python
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.

```python
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.

```mermaid
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
```

```python
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.

```python
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

```bash
pip install open-guard-python[mcp]
```

See [§8 above](#8-mcp-tool-call-adapter).

### FastAPI

```bash
pip install open-guard-python[fastapi]
```

Mount the admin REST surface:

```python
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:

```python
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:

```bash
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

```bash
pip install open-guard-python[client]
```

```python
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:

```python
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

```python
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

```bash
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](LICENSE) © 2026 Avinash Singh
