Metadata-Version: 2.4
Name: suvra
Version: 0.1.7
Summary: Policy-first execution guardrails for autonomous agents
License-Expression: MIT
Project-URL: Homepage, https://github.com/suvra-core/suvra
Project-URL: Repository, https://github.com/suvra-core/suvra
Project-URL: Documentation, https://github.com/suvra-core/suvra#readme
Project-URL: Bug Tracker, https://github.com/suvra-core/suvra/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.111.0
Requires-Dist: uvicorn>=0.30.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: pyyaml>=6.0.1
Requires-Dist: httpx>=0.27.0
Requires-Dist: jinja2>=3.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.2.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
Dynamic: license-file

# Suvra

> Secure, policy-first execution guardrails for autonomous agents.

![Status](https://img.shields.io/badge/status-active-success)
![PyPI](https://img.shields.io/pypi/v/suvra)
![Python](https://img.shields.io/pypi/pyversions/suvra)

Suvra is a guardrail layer that sits between an agent and real-world side effects.
It evaluates declarative actions against `policy.yaml`, enforces allow/deny/approval decisions, executes deterministically, and logs an audit trail to SQLite.

This matters when you want AI automation with explicit control,
deterministic enforcement, explainable decisions, and rollback-ready operations.

Suvra turns probabilistic agent intent into policy-governed, auditable execution.

## Install

```bash
pip install suvra
```

Requires Python 3.9+.

## Core Guarantees

- Deny-by-default enforcement.
- Deterministic policy enforcement with no LLM in the enforcement path.
- Approval gating for sensitive actions.
- Workspace jail confinement with configurable root via `SUVRA_WORKSPACE_DIR`.
- Fail-closed guard-boundary actions (`shell.exec`, `email.delete`, `secrets.read`).
- SQLite audit trail.

## Architecture
Agent → Suvra Policy Engine → Executor → Audit Log

## Table of Contents

- [Features](#features)
- [Install](#install)
- [Core Guarantees](#core-guarantees)
- [Project Layout](#project-layout)
- [Quickstart](#quickstart)
  - [Embedded SDK (in-process)](#embedded-sdk-in-process)
  - [Remote SDK (HTTP)](#remote-sdk-http)
- [Examples](#examples)
  - [Embedded Guard usage](#embedded-guard-usage)
  - [Remote Guard usage](#remote-guard-usage)
  - [OpenClaw adapter example](#openclaw-adapter-example)
  - [guarded_tool decorator example](#guarded_tool-decorator-example)
- [API Usage](#api-usage)
- [CLI Usage](#cli-usage)
- [Dashboard](#dashboard)
- [Enforcement Modes](docs/enforcement_modes.md)
- [Feedback](#feedback)
- [License](#license)

## Features

- Policy-based enforcement with deny-by-default fallback.
- Decision modes: `allow`, `deny`, `needs_approval`.
- Deterministic action executors:
  - `fs.write_file`
  - `fs.delete_file`
  - `http.request` (GET only, redirects blocked)
- Policy/simulation-only action types (recognized by policy + `/simulate`, execution fails closed):
  - `shell.exec`
  - `email.delete`
  - `secrets.read`
- Dry-run validation and execution flows.
- Approval workflow endpoints and rollback endpoint.
- Embedded SDK (`Guard(policy=...)`) and Remote SDK (`Guard(url=...)`).
- SQLite audit logging (`data/audit.db`) with durable rollback payload persistence.
- FastAPI service and a direct-library CLI.

## Project Layout

- `suvra/app/main.py` - FastAPI app
- `suvra/core/policy.py` - policy loading/evaluation
- `suvra/core/executors/` - action executors
- `suvra/core/audit.py` - SQLite audit log
- `suvra/core/service.py` - orchestration layer
- `suvra/sdk/guard.py` - SDK guard (embedded + remote)
- `suvra/sdk/decorators.py` - `guarded_tool` decorator
- `suvra/integrations/openclaw.py` - OpenClaw adapter
- `suvra/cli.py` - CLI tool
- `policy.yaml` - default single-file policy (used when `SUVRA_POLICY_PATH` is unset)
- `policies/` - example scoped policy directory (`10-global.yaml`, `20-workspace-delete.yaml`); activate with `SUVRA_POLICY_PATH=./policies`
- `policies.yaml` - original flat policy preserved for reference
- `examples/` - sample action payloads
- `tests/` - pytest tests

## Quickstart

### Embedded SDK (in-process)

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e .
```

```python
from suvra import Guard

guard = Guard(policy="policy.yaml", db_path="data/audit.db")

result = guard.execute(
    {
        "action_id": "quickstart-embedded",
        "type": "fs.write_file",
        "params": {
            "path": "workspace/quickstart_embedded.txt",
            "content": "hello from embedded mode"
        },
        "meta": {"actor": "quickstart"},
        "dry_run": False,
    }
)

print(result)
```

### Remote SDK (HTTP)

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e .
uvicorn suvra.app.main:app --reload
# or
suvra serve
```

```python
from suvra import Guard

guard = Guard(url="http://127.0.0.1:8000")

result = guard.validate(
    {
        "action_id": "quickstart-remote",
        "type": "fs.write_file",
        "params": {
            "path": "workspace/quickstart_remote.txt",
            "content": "hello from remote mode"
        },
        "meta": {"actor": "quickstart"},
        "dry_run": True,
    }
)

print(result)
```

Environment config:

- `SUVRA_POLICY_PATH` (default: `policy.yaml`)
- `SUVRA_DB_PATH` (default: `data/audit.db`)
- `SUVRA_WORKSPACE_DIR` (default: `workspace`)

### OpenClaw Quickstart: safest defaults

Suvra starts deny-by-default and only allows narrow actions from the built-in safe template on first run.
Writes must target the configured workspace root (default: `workspace/`), and deletes require approval.

```bash
docker run --rm -p 8000:8000 \
  -e SUVRA_ENV=dev \
  -e SUVRA_DB_PATH=/app/data/audit.db \
  -v "$(pwd)/workspace:/app/workspace" \
  -v "$(pwd)/data:/app/data" \
  suvra:latest
```

Use the configured workspace root (default `workspace/`) as the writable mount for OpenClaw tasks.

## Examples

### Embedded Guard usage

```python
from suvra import Guard

guard = Guard(policy="policy.yaml", db_path="data/audit.db")

action = {
    "action_id": "sdk-embedded-1",
    "type": "fs.write_file",
    "params": {"path": "workspace/embedded.txt", "content": "hello"},
    "meta": {"actor": "sdk"},
    "dry_run": False,
}

result = guard.execute(action)
print(result)
```

### Remote Guard usage

```python
from suvra import Guard

guard = Guard(url="http://127.0.0.1:8000")

action = {
    "action_id": "sdk-remote-1",
    "type": "fs.write_file",
    "params": {"path": "workspace/remote.txt", "content": "hello"},
    "meta": {"actor": "sdk"},
    "dry_run": True,
}

result = guard.validate(action)
print(result)
```

`Guard` raises `ValueError` if both `policy` and `url` are provided.

### OpenClaw adapter example

```python
from suvra import Guard
from suvra.integrations.openclaw import SuvraExecutor

guard = Guard(policy="policy.yaml", db_path="data/audit.db")
executor = SuvraExecutor(guard)

execute_result = executor.execute(
    action_type="fs.write_file",
    params={"path": "workspace/demo/oc_adapter.txt", "content": "openclaw adapter"},
)

validate_result = executor.validate(
    action_type="fs.write_file",
    params={"path": "workspace/demo/oc_adapter.txt", "content": "openclaw adapter"},
)

print(execute_result)
print(validate_result)
```

### guarded_tool decorator example

```python
from pathlib import Path

from suvra import Guard, guarded_tool

guard = Guard(policy="policy.yaml", db_path="data/audit.db")

@guarded_tool(guard, "fs.write_file")
def write_file(path: str, content: str) -> str:
    target = Path(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content)
    return str(target)

written = write_file(path="workspace/decorated.txt", content="decorator write")
print(written)
```

## API Usage

Run server:

```bash
uvicorn suvra.app.main:app --reload
# or
suvra serve
```

Docker:

```bash
docker build -t suvra:latest .
docker run --rm -p 8000:8000 \
  -e SUVRA_POLICY_PATH=/app/policy.yaml \
  -e SUVRA_DB_PATH=/app/data/audit.db \
  -v "$(pwd)/policy.yaml:/app/policy.yaml:ro" \
  -v "$(pwd)/data:/app/data" \
  suvra:latest
```

Docker Compose:

```bash
docker compose up --build
```

FastAPI endpoints:

- `GET /health`
- `POST /actions/validate`
- `POST /actions/execute`
- `POST /approvals/request`
- `POST /approvals/{approval_id}/approve`
- `POST /approvals/{approval_id}/deny`
- `GET /approvals/{approval_id}`
- `POST /rollback/{action_id}`
- `GET /audit`

Rollback is audit-backed: when an executor returns a rollback payload, Suvra persists it in `audit_events.rollback_payload` and treats the audit DB as the canonical rollback store. A fresh process can roll back prior actions as long as the same `data/audit.db` is available.

Canonical `ActionRequest` shape:

```json
{
  "action_id": "string",
  "type": "string",
  "params": {},
  "meta": {
    "actor": "string",
    "reason": "optional string",
    "approval_id": "optional string"
  },
  "tenant_id": "optional string",
  "business_unit": "optional string",
  "domain": "optional string",
  "agent": "optional string",
  "user": "optional string",
  "role": "optional string",
  "workspace": "optional string",
  "environment": "optional string",
  "labels": ["optional", "labels"],
  "dry_run": false
}
```

Identity-aware policy constraints are optional and deterministic: `tenant_id`, `business_unit`, `domain`, `agent`, `user`, `role`, `workspace`, and `environment` use exact string matching, while `labels` requires every listed label to be present on the action after trim/dedupe/sort normalization.

## Policy Scope Model

Suvra still supports the legacy single-file `policy.yaml` format with no scope block.

For scoped policies, you can point `SUVRA_POLICY_PATH` (or the engine `policy_path`) at a directory containing multiple policy files. In scoped directory mode, every policy file must declare exactly one scope object; stray unscoped legacy files are rejected so they cannot silently apply as global rules.

```yaml
scope:
  level: tenant
  tenant_id: acme

defaults:
  mode: deny

rules:
  - id: tenant_write
    effect: allow
    type: fs.write_file
    constraints:
      path_prefix: workspace/
      max_bytes: 2048
```

Supported scope levels, from least to most specific:

- `global`
- `tenant` (`tenant_id`)
- `business_unit` (`business_unit`)
- `domain` (`domain`)
- `workspace` (`workspace`)
- `agent` (`agent`)
- `environment` (`environment`)

Applicable scoped policies are combined deterministically in this order: `global`, `tenant`, `business_unit`, `domain`, `workspace`, `agent`, `environment`, with stable path ordering inside each level bucket. Rule evaluation remains first-match-wins against that ordered effective rule set.

Scoped directory mode keeps a deny-by-default fallback even when more specific scoped files declare `defaults.mode: allow`; explicit matching rules are still required to allow an action.

Guard-boundary-only action parameter shapes (recognized for policy and simulation, unsupported on execute):

- `shell.exec`:
  - `command` (string, required)
  - `args` (list[string], optional)
  - `cwd` (string, optional)
  - `dry_run` (bool, optional)
- `email.delete`:
  - `provider` (string, optional)
  - `message_id` (string, required)
  - `mailbox` (string, optional)
  - `reason` (string, optional)
  - `dry_run` (bool, optional)
- `secrets.read`:
  - `name` (string, required)
  - `purpose` (string, optional)
  - `dry_run` (bool, optional)

Backward compatibility is preserved for deprecated top-level `actor` and `approval_id`, but clients should migrate to `meta.actor` and `meta.approval_id`.

Validate (dry-run by design):

```bash
curl -s -X POST http://127.0.0.1:8000/actions/validate \
  -H 'content-type: application/json' \
  -d @examples/action_write_ok.json
```

Execute action:

```bash
curl -s -X POST http://127.0.0.1:8000/actions/execute \
  -H 'content-type: application/json' \
  -d @examples/action_write_ok.json
```

Note: `shell.exec`, `email.delete`, and `secrets.read` are intentionally unsupported by execution.  
`POST /actions/execute` fails closed with `EXECUTION_ERROR` for those action types in all modes.

Execute in dry-run mode (no side effects):

```bash
jq '. + {dry_run: true}' examples/action_write_ok.json | \
  curl -s -X POST http://127.0.0.1:8000/actions/execute \
  -H 'content-type: application/json' \
  -d @-
```

Simulation-only policy examples (execution still unsupported):

```yaml
rules:
  - id: shell_git_status_only
    effect: allow
    type: shell.exec
    constraints:
      allow_commands: ["git status"]

  - id: email_delete_inbox_approval
    effect: needs_approval
    type: email.delete
    constraints:
      allow_providers: ["gmail"]
      allow_mailboxes: ["INBOX"]

  - id: secrets_openai_key_approval
    effect: needs_approval
    type: secrets.read
    constraints:
      allow_names: ["OPENAI_API_KEY"]
```

Approval flow example (default `policy.yaml` has `fs.delete_file` as `needs_approval`):

```bash
curl -s -X POST http://127.0.0.1:8000/actions/validate \
  -H 'content-type: application/json' \
  -d @examples/action_delete_file.json
```

Delete with approval + rollback demo (using default `policy.yaml`):

```bash
# 1) Write a file
curl -s -X POST http://127.0.0.1:8000/actions/execute \
  -H 'content-type: application/json' \
  -d @examples/action_write_ok.json

# 2) Execute delete without approval_id -> returns decision=needs_approval + approval_id
curl -s -X POST http://127.0.0.1:8000/actions/execute \
  -H 'content-type: application/json' \
  -d @examples/action_delete_file.json

# 3) Approve it
curl -s -X POST http://127.0.0.1:8000/approvals/<approval_id>/approve \
  -H 'content-type: application/json' \
  -d '{"decided_by":"admin","note":"approved"}'

# 4) Re-execute delete with meta.approval_id
jq --arg approval_id "<approval_id>" '.meta += {approval_id:$approval_id}' examples/action_delete_file.json | \
  curl -s -X POST http://127.0.0.1:8000/actions/execute \
  -H 'content-type: application/json' \
  -d @-

# 5) Roll back the delete by action_id
curl -s -X POST http://127.0.0.1:8000/rollback/delete-1 \
  -H 'content-type: application/json' \
  -d '{"dry_run":false}'

# 6) Verify restored file content
cat workspace/demo/hello.txt
```

The rollback call above works across service restarts because the rollback payload is loaded from SQLite, not process memory.

## CLI Usage
The CLI wraps the same EnforcementEngine used by the SDK and API.

Validate from JSON file:

```bash
python -m suvra.cli validate examples/action_write_ok.json
```

Execute from JSON file:

```bash
python -m suvra.cli execute examples/action_write_ok.json
```

Execute dry-run:

```bash
python -m suvra.cli execute examples/action_write_ok.json --dry-run
```

List built-in starter policy packs:

```bash
suvra policy list
```

Initialize `./policy.yaml` from the default starter pack:

```bash
suvra policy init
```

Copy a named starter pack to a custom location:

```bash
suvra policy use coding --out ./policy.coding.yaml
```

Built-in starter packs are file templates only. They do not change runtime policy behavior unless you explicitly copy one into place.

Run tests:

```bash
pytest
```

## Dashboard

Start the server:

```bash
suvra serve
```

Then open `http://127.0.0.1:8000/dashboard`.

**Overview** — counts for total events, pending approvals, allow/deny/needs_approval decisions, and rollbacks.

**Audit Explorer** (`/dashboard/audit`) — searchable, paginated audit event table with:
- Free-text search across all fields
- Exact-match filters for Actor, Action Type, Decision, Status, Agent, Tenant ID, and Domain
- Per-row expandable detail view showing identity context (agent, tenant, domain, labels, etc.) and decision reasons
- Rollback button on rows with a saved rollback payload
- CSV export

**Approvals** (`/dashboard/approvals`) — review and decide pending actions with status tabs (pending / approved / denied / all), approve / deny / approve+execute buttons, and per-row identity detail.

**Policy Viewer** (`/dashboard/policy`) — read-only view of the active policy:
- Single-file mode: shows the policy YAML/JSON content directly.
- Scoped directory mode: shows a table of all policy files with their scope level, scope identity value, default mode, and rule count. Each row is expandable to see the rules inline. Enable by starting with `SUVRA_POLICY_PATH=./policies suvra serve` (the repo ships a ready-to-use `policies/` directory).

**Simulator** (`/dashboard/simulate`) — test any action JSON against active or custom policy without executing side effects. Shows decision badge, matched rule, reasons, constraint checks, and full identity context.

**Enforcement mode banner** — visible when `SUVRA_MODE` is not `strict`.

## Feedback

Suvra is currently in soft beta.

If you're experimenting with it in OpenClaw or other agent frameworks and have feedback, ideas, or edge cases to report, feel free to reach out.

Early builder feedback is especially appreciated.

## Documentation Rule

Any change that affects:

- API endpoints
- Policy schema
- Enforcement logic
- Executors
- Enforcement modes
- Audit schema
- Dashboard routes
- SDK behavior
- Metrics

MUST update `docs/SUVRA_CONTEXT.md` in the same commit.

Commits modifying `core/` without updating `SUVRA_CONTEXT.md` are incomplete.

## License

MIT License. See the LICENSE file for details.
