Metadata-Version: 2.4
Name: guardrail-dv
Version: 0.7.0
Summary: Deterministic safety checks for LLM-generated prompts, SQL and Python — a pure library, evaluated in-process
License-Expression: MIT AND LicenseRef-Llama-4-Community
License-File: LICENSE
License-File: LICENSE-LLAMA
License-File: NOTICE
Keywords: guardrail,llm,prompt-injection,sql-injection,text-to-sql,security,validation,sqlglot,onnx
Author: dview.io
Author-email: hema@dview.io
Requires-Python: >=3.12,<3.14
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Security
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Provides-Extra: all
Provides-Extra: pii
Provides-Extra: routers
Provides-Extra: storage
Requires-Dist: langid (>=1.1.6,<2.0.0)
Requires-Dist: onnxruntime (==1.28.0)
Requires-Dist: presidio-analyzer (>=2.2.355,<3.0.0) ; extra == "all"
Requires-Dist: presidio-analyzer (>=2.2.355,<3.0.0) ; extra == "pii"
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
Requires-Dist: spacy (>=3.7.0,<4.0.0) ; extra == "all"
Requires-Dist: spacy (>=3.7.0,<4.0.0) ; extra == "pii"
Requires-Dist: sqlglot (==25.34.1)
Requires-Dist: tokenizers (==0.22.2)
Project-URL: Changelog, https://github.com/dview-io/guardrail/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/dview-io/guardrail
Project-URL: Issues, https://github.com/dview-io/guardrail/issues
Project-URL: Repository, https://github.com/dview-io/guardrail
Description-Content-Type: text/markdown

# guardrail-dv

Deterministic safety checks for LLM-generated prompts, SQL and Python — an in-process library, no
service to deploy.

```python
import guardrail

guardrail.init()

with guardrail.turn(question="revenue by region", allowed_tables=["sales.orders"]):
    guardrail.guard_prompt("revenue by region")
    guardrail.guard_sql("SELECT region, SUM(revenue) FROM sales.orders GROUP BY region")
```

A blocked check raises `GuardrailBlocked`. Nothing else changes in your code.

---

## Install

```bash
pip install guardrail-dv
```

Python 3.12 or 3.13. The injection classifier ships inside the wheel — no download, no model
server, no network call at runtime.

Optional entity scan over personal data (adds Presidio and spaCy, ~200MB):

```bash
pip install "guardrail-dv[pii]"
```

## Quick start

Call `init()` once at startup, then open a turn per user question:

```python
import guardrail
from guardrail import GuardrailBlocked

guardrail.init()          # loads the rulebook and the classifier

def answer(question: str) -> str:
    with guardrail.turn(question=question, allowed_tables=["sales.orders"]) as turn:
        guardrail.guard_prompt(question)

        sql = llm.write_sql(question)
        guardrail.guard_sql(sql)
        rows = warehouse.execute(sql)

        code = llm.write_pandas(question)
        guardrail.guard_python(code)

        text = llm.summarise(rows)
        guardrail.guard_output(text)
        guardrail.guard_grounding(text, data=rows)
        return text

try:
    answer("revenue by region")
except GuardrailBlocked as blocked:
    print(blocked.decision.message)        # for the user
    print(blocked.decision.llm_feedback)   # for the model, on a retry
```

## The checks

| Function | Checks | Blocks by default |
| --- | --- | :---: |
| `guard_prompt(text)` | prompt injection, jailbreaks, exfiltration, destructive intent | yes |
| `guard_sql(sql)` | read-only, single statement, table scope, denied columns, scan budget | yes |
| `guard_python(code)` | forbidden imports and calls, dunder access | yes |
| `guard_output(text)` | credentials, secrets, exfiltration beacons, personal data | no |
| `guard_grounding(text, data=…)` | whether the answer is supported by the data and the turn | no |

Every one has an async twin — `aguard_sql`, `aguard_python`, `aguard_output`, `aguard_grounding`.

They can also be called as methods on the turn, which is equivalent:

```python
with guardrail.turn(question=q, allowed_tables=tables) as turn:
    turn.prompt(q)
    turn.sql(sql)
    turn.grounding(text, data=rows)
```

### Turn options

```python
guardrail.turn(
    question="revenue by region",     # used by the language and intent rules
    allowed_tables=["sales.orders"],  # the scope guard_sql enforces
    denied_columns=["ssn"],           # columns this user may not read
    dialect="trino",                  # SQL dialect; defaults to trino
    source="cortex",                  # who is asking
    conversation_id="c-1",            # your own ids, carried onto every decision
    raise_on_block=False,             # return verdicts instead of raising
    expect={"injection", "sql"},      # phases that must run, recorded if they don't
)
```

## Reading a decision

Every check returns the same object, whether or not it blocked:

```python
decision = guardrail.guard_sql(sql)

decision.verdict         # PASSED | WARNING | BLOCKED | SKIPPED
decision.action          # PROCEED | REVIEW  | STOP
decision.message         # for the user, or None
decision.llm_feedback    # for the model on a retry, or None
decision.violated_rules  # ["hard.sql.allowed_statements"]
decision.retryable       # True | False | None — see below
decision.confidence      # 0.0–1.0
decision.attempt         # 1, or higher after a retry
```

`turn.decisions` holds every decision the turn produced, all sharing one `review_id`. Guardrail
stores nothing — write them wherever you already write things:

```python
with guardrail.turn(question=q, conversation_id=conversation.id) as turn:
    ...

for decision in turn.decisions:
    my_table.insert(decision)
```

## Retrying a blocked phase

**If you have your own retry loop**, read two fields and keep your loop:

```python
except GuardrailBlocked as blocked:
    if blocked.decision.retryable is False:
        raise                                   # rewriting cannot clear this
    sql = llm.rewrite(sql, blocked.decision.llm_feedback)
```

`retryable` is `None` when no rule fired, and `False` when the rules that fired cannot be cleared by
rewriting — a prompt injection, for instance, because the artifact is the user's own words.

**If you do not**, hand the library a callback and it runs the loop for you:

```python
with guardrail.turn(question=q, allowed_tables=tables) as turn:
    decision = turn.sql(sql, regenerate=lambda fb: llm.rewrite(sql, fb.feedback))
```

It re-checks only the failed phase, keeps every attempt on `turn.decisions`, and stops on the first
of: the phase passing, `max_attempts` (default 3), the turn's budget (default 4), an unretryable
rule, the model returning the same artifact, or the callback raising.

## Across services

A turn is a `ContextVar`, so it ends at the process boundary. To keep one `review_id` across an HTTP
hop:

```python
# caller — empty when no turn is open, so merge it unconditionally
headers.update(guardrail.turn_headers())

# callee — pure ASGI, opens no turn of its own
app.add_middleware(guardrail.TurnPropagationMiddleware)
```

Outside HTTP, use `with guardrail.adopt(review_id):`.

## Configuration

Everything has a working default; you can run with no configuration at all. To change something,
pass a dict to `init()`:

```python
guardrail.init(config={
    "enforce": {"pii": True},
    "regeneration": {"max_attempts": 2},
    "prompt_guard": {"enabled": False},
})
```

| Section | Key | Default |
| --- | --- | --- |
| `app` | `default_sql_dialect` | `trino` |
| `policy` | `file_path` | the packaged `policies.yaml` |
| | `reload_interval_seconds` | `60` |
| | `overrides` | `{}` — per-rule `enabled`, `mode`, `default_severity` |
| | `enforcement` | `None` — set `observe` to make every rule record-only |
| `prompt_guard` | `enabled` | `true` |
| | `max_tokens` / `window_overlap` | `512` / `64` |
| `enforce` | `injection`, `sql`, `python` | `true` |
| | `pii`, `grounding` | `false` — recorded, never raised |
| `regeneration` | `max_attempts` | `3` (hard ceiling 3) |
| | `max_per_turn` | `4` |
| | `retry_on_warning` | `false` |
| `memory` | `max_entries` / `max_turns` | `512` / `512` |

Or point `GUARDRAIL_CONFIG_FILE_PATH` at a YAML file with the same shape.

Rule patterns and thresholds live in the rulebook, not here — `src/guardrail/resources/policies.yaml`,
30 rules, stamped on every decision as `policy_version`.

## Environment variables

All optional. There is no `.env` file and none is read.

| Variable | Default | Effect |
| --- | --- | --- |
| `GUARDRAIL_ENABLED` | `true` | `false` turns everything off; every check returns `SKIPPED` |
| `GUARDRAIL_ENFORCEMENT` | — | `observe` records without blocking |
| `GUARDRAIL_MAX_REGENERATIONS` | `3` | retry ceiling |
| `GUARDRAIL_CONFIG_FILE_PATH` | — | path to a YAML config |
| `GUARDRAIL_LOG_LEVEL` | `INFO` | log level |

Switching Guardrail off leaves no evidence that the checks ran, so the turns read afterwards as
unguarded. Prefer `GUARDRAIL_ENFORCEMENT=observe` if you want to keep the record.

## CLI

```bash
guardrail status                                   # what this install is enforcing
guardrail check sql "DROP TABLE users"             # exits 1 if blocked
echo "$SQL" | guardrail check sql - --allowed-tables sales.orders
```

## Verdicts

| Verdict | Action | Meaning |
| --- | --- | --- |
| `PASSED` | `PROCEED` | nothing fired |
| `SKIPPED` | `PROCEED` | the check could not run, or Guardrail is off |
| `WARNING` | `REVIEW` | something fired, but not enough to stop |
| `BLOCKED` | `STOP` | raises `GuardrailBlocked` where the check is enforced |

## Exceptions

| Exception | Raised when |
| --- | --- |
| `GuardrailBlocked` | an enforced check blocked; carries `.decision` |
| `NotInitialised` | `init()` could not load the rulebook or the classifier |
| `NoCurrentTurn` | a check was called with no open turn |
| `PolicyException` | the rulebook is missing or empty |

## License

The code is MIT — see [LICENSE](LICENSE).

The bundled classifier is `Llama-Prompt-Guard-2-22M-dview-v3`, a fine-tune of Meta's Llama Prompt
Guard 2 (22M), and is **not** MIT. Built with Llama. See [NOTICE](NOTICE) and
[LICENSE-LLAMA](LICENSE-LLAMA) for the terms that apply to the weights.

Build tooling for the classifier is documented in [scripts/README.md](scripts/README.md).

