Metadata-Version: 2.4
Name: suvra
Version: 0.1.0
Summary: Suvra v0 secure execution guardrail layer for autonomous agents
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
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"

# Suvra

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

![Version](https://img.shields.io/badge/version-0.1.0-blue)
![Status](https://img.shields.io/badge/status-active-success)
![Python](https://img.shields.io/badge/python-3.10%2B-informational)

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.

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

## Table of Contents

- [Features](#features)
- [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)
- [Contributing](#contributing)
- [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)
- 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`).
- 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
- `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
```

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

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`

Canonical `ActionRequest` shape:

```json
{
  "action_id": "string",
  "type": "string",
  "params": {},
  "meta": {
    "actor": "string",
    "reason": "optional string",
    "approval_id": "optional string"
  },
  "dry_run": false
}
```

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

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

Run tests:

```bash
pytest
```

## Dashboard

A hosted dashboard demo is forthcoming.

## Contributing

Contributions are welcome. For now:

1. Fork the repository.
2. Create a feature branch.
3. Add or update tests in `tests/`.
4. Run `pytest` locally.
5. Open a pull request with a clear summary.

## 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

License information will be published prior to open-source release.
