Metadata-Version: 2.4
Name: initguard
Version: 2026.3.1
Summary: Embedded ABAC policy engine for AI agents
Project-URL: Repository, https://github.com/vladkesler/initguard
Project-URL: Issues, https://github.com/vladkesler/initguard/issues
Author-email: vladimir kesler <contact@initrunner.ai>
License-Expression: MIT OR Apache-2.0
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
Keywords: abac,agent,ai,authorization,cel,policy,yaml
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security
Requires-Python: >=3.11
Requires-Dist: cel-python>=0.5.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.15.7; extra == 'dev'
Requires-Dist: ty>=0.0.1a7; extra == 'dev'
Description-Content-Type: text/markdown

# initguard

Embedded ABAC policy engine for AI agents.

[![PyPI](https://img.shields.io/pypi/v/initguard)](https://pypi.org/project/initguard/)
[![Python](https://img.shields.io/pypi/pyversions/initguard)](https://pypi.org/project/initguard/)
[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT)

Write authorization policies in YAML with [CEL](https://cel.dev/) conditions, evaluate them in-process. No sidecar, no HTTP, no external service. Policies use `when`/`unless` clauses for readable rules and return structured `Decision` objects with human-readable reasons and advice.

## Install

```bash
pip install initguard
```

## Quick start

Create a policy file:

```yaml
# policies/tool_policy.yaml
apiVersion: initguard/v1
kind: ResourcePolicy
resource: tool
rules:
  - actions: ["execute"]
    effect: allow
    roles: ["agent"]
    when: request.resource.attr.tool_type in ["http", "search"]

  - actions: ["execute"]
    effect: deny
    roles: ["agent"]
    when: request.resource.attr.tool_type == "shell"
    unless: request.principal.attr.tags.exists(t, t == "trusted")
    advice: "Shell tools require the 'trusted' tag."
```

Load and evaluate:

```python
from initguard import PolicyEngine, Principal, load_policies

engine = PolicyEngine(load_policies("./policies"))

agent = Principal(
    id="agent:helper",
    roles=["agent"],
    attrs={"tags": ["basic"]},
)

decision = engine.check(agent, "tool", "execute", resource_attrs={"tool_type": "shell"})

print(decision.allowed)  # False
print(decision.reason)   # "denied by tool_policy.yaml:rules[1]"
print(decision.advice)   # "Shell tools require the 'trusted' tag."
```

## Policy format

Three document kinds, all in YAML.

### Schema (optional)

Lint your policies against known attribute names. Catches typos at load time.

```yaml
apiVersion: initguard/v1
kind: Schema
principals:
  agent:
    attrs:
      team: string
      tags: list
resources:
  tool:
    attrs:
      tool_type: string
    actions: [execute]
```

### DerivedRoles

Conditional role elevation using CEL. A definition matches when `when` is true and `unless` is false (or absent). Names must be globally unique.

```yaml
apiVersion: initguard/v1
kind: DerivedRoles
name: agent_roles
definitions:
  - name: trusted_agent
    parentRoles: ["agent"]
    when: request.principal.attr.tags.exists(t, t == "trusted")

  - name: same_team
    parentRoles: ["agent"]
    when: request.principal.attr.team != ""
    unless: request.principal.attr.team != request.resource.attr.team
```

### ResourcePolicy

Allow/deny rules for a resource kind. Deny always wins over allow. Every rule must declare at least one of `roles` or `derivedRoles`.

```yaml
apiVersion: initguard/v1
kind: ResourcePolicy
resource: tool
importDerivedRoles: [agent_roles]
rules:
  - actions: ["execute"]
    effect: allow
    derivedRoles: ["trusted_agent"]

  - actions: ["execute"]
    effect: deny
    roles: ["agent"]
    when: request.resource.attr.tool_type in ["shell", "python"]
    unless: request.principal.attr.tags.exists(t, t == "trusted")
    advice: "Requires the 'trusted' tag."
```

Derived roles are **scoped to imports** -- a ResourcePolicy only sees roles from the sets it explicitly imports, preventing privilege leakage across policy domains.

## API

### `load_policies(policy_dir) -> PolicySet`

Load all `*.yaml` / `*.yml` files from a directory. Files are loaded in sorted order for deterministic precedence. All CEL expressions are compiled at load time. Raises `PolicyLoadError` on invalid YAML, bad CEL syntax, duplicate role names, or broken references.

### `PolicyEngine(policy_set)`

```python
engine = PolicyEngine(policy_set)
```

#### `engine.check(principal, resource_kind, action, resource_id="*", resource_attrs=None) -> Decision`

Evaluate policies and return a `Decision`. Also available as `check_async` for async contexts.

#### `engine.info() -> EngineInfo`

Returns loaded policy summary: resource kinds, derived role sets, policy/rule counts, schema presence.

### `Decision`

| Field | Type | Description |
|-------|------|-------------|
| `allowed` | `bool` | Whether the action is permitted |
| `reason` | `str` | Human-readable explanation, always populated |
| `matched_rule` | `str` | Stable rule identifier, e.g. `"tool_policy.yaml:rules[1]"` |
| `advice` | `str` | From the rule's `advice` field, or empty |

### `Principal`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `str` | Identity string, e.g. `"agent:code-reviewer"` |
| `roles` | `list[str]` | Role list, e.g. `["agent", "team:platform"]` |
| `attrs` | `dict[str, Any]` | Attributes for CEL conditions |

## CEL expressions

Conditions use [Google Common Expression Language](https://cel.dev/). Supported patterns:

```python
# List membership
request.resource.attr.tool_type in ["shell", "python"]

# Existential quantifier
request.principal.attr.tags.exists(t, t == "trusted")

# Attribute equality
request.principal.attr.team == request.resource.attr.team

# Boolean operators
!request.principal.attr.tags.exists(t, t == "admin") && request.resource.attr.tool_type == "shell"
```

Missing attributes in CEL expressions evaluate to `false` (condition not met), not an error.

## Development

```bash
uv sync --all-extras
uv run pytest tests/ -v
uv run ruff check . && uv run ruff format --check .
```

## License

MIT OR Apache-2.0
