Metadata-Version: 2.4
Name: rein-strands
Version: 0.4.0
Summary: Deterministic, no-LLM tool-call guardrail for Strands agents, backed by the rein engine.
Project-URL: Homepage, https://github.com/SametAtas/rein-strands
Project-URL: Repository, https://github.com/SametAtas/rein-strands
Project-URL: Issues, https://github.com/SametAtas/rein-strands/issues
Author: Abdulsamet Atas
License: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,guardrails,rein,security,strands,strands-agents
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: rein-engine>=0.3.0
Requires-Dist: strands-agents>=1.40
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: strands-agents-tools; extra == 'dev'
Description-Content-Type: text/markdown

# rein-strands

[![CI](https://github.com/SametAtas/rein-strands/actions/workflows/ci.yml/badge.svg)](https://github.com/SametAtas/rein-strands/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/rein-strands.svg)](https://pypi.org/project/rein-strands/)
[![Python](https://img.shields.io/pypi/pyversions/rein-strands.svg)](https://pypi.org/project/rein-strands/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

A deterministic, no-LLM guardrail for [Strands](https://strandsagents.com) agents.
It reviews the code or command a tool is about to run and **cancels the call before
the tool executes** when the verdict crosses a severity threshold. Backed by the
[rein](https://github.com/SametAtas/rein) engine.

No LLM judges the action, so the verdict is the same every run and you can see
exactly why a call was stopped. The same check behaves identically whether a
person, CI, or the agent triggers it, which keeps accountability clear.

## See it block a real agent

`examples/agent_demo.py` runs an actual Strands `Agent` (a scripted model stands
in for the LLM, so no API key is needed) and lets rein gate its tool calls:

```text
[1] dangerous code
    agent tried: save_code('import os\nos.system(user_input)\n')
    -> BLOCKED before execution; tool body never ran
       rein blocked tool 'save_code': security.os-system (high): os.system/os.popen
       invokes a shell; use subprocess with a list and shell=False.

[2] clean code
    agent tried: save_code('def add(a, b):\n    return a + b\n')
    -> allowed; tool ran
```

The dangerous tool call is cancelled at the boundary; the tool function never
runs. The clean one goes through untouched.

## Install

```bash
pip install rein-strands
```

## Use

Attach the hook to your agent. That is the whole integration:

```python
from strands import Agent
from strands_tools import shell, file_write, python_repl

from rein_strands import ReinToolGuard

agent = Agent(
    model=model,
    tools=[shell, file_write, python_repl],
    hooks=[ReinToolGuard()],   # blocks HIGH+ verdicts before the tool runs
)
```

## What it reviews

| Tool | Field reviewed | Analysis |
|------|----------------|----------|
| `python_repl` | `code` | Full rein code analysis (stateful session, so undefined-name is not enforced) |
| `file_write` | `content` (`.py`) | Full rein code analysis, fails closed if the file will not parse |
| `editor` | `file_text` / `new_str` (`.py`) | Full rein code analysis on the added text |
| `shell` | `command` | Secrets, plus full analysis of any inline `python -c "..."` code |
| custom | `content` / `code` (no path) | Treated as Python so dangerous code is still caught |
| any | non-`.py` file content | Secrets only (the Python AST checks do not apply) |

rein catches hard-coded secrets, unsafe calls (`os.system`, `eval`, `pickle`,
weak hashes, and so on), undefined names, and (with `project_root`) hallucinated
imports. Each finding carries the reason and a remedy, not just an alert.

## Modes and threshold

```python
from rein_strands import ReinToolGuard, Severity

# Report only, never block (human-in-the-loop, where a person owns the call):
ReinToolGuard(mode="audit", on_finding=lambda d: print(d.reason))

# Stricter: also block MEDIUM findings (e.g. weak hashes, hallucinated imports):
ReinToolGuard(block_at=Severity.MEDIUM)
```

- `mode="enforce"` (default) cancels a call whose verdict is at or above `block_at`.
- `mode="audit"` never cancels and only reports findings.
- `block_at` defaults to `Severity.HIGH`.

## Catching hallucinated imports

Pass `project_root` and rein also checks every Python import the agent writes
against the project's stdlib, declared dependencies, and own modules, so a
hallucinated or undeclared module is caught before the file is written or run:

```python
ReinToolGuard(project_root=".", block_at=Severity.MEDIUM)
```

This needs the project to declare dependencies (a `pyproject.toml` `[project]`
table or a `requirements*.txt`); without one rein cannot know what is installed,
so the import check stays inert rather than guessing.

## Deep mode: external scanners

By default the guard runs rein's fast, in-process native checks. Opt into the
external scanners rein integrates (ruff, bandit, gitleaks, semgrep) and they run
over the tool's content before it executes, folding their findings into the same
verdict:

```python
ReinToolGuard(scanners=("bandit", "gitleaks"))
```

This is off by default on purpose: external scanners cost real time per call, so
it is opt-in and you choose which to run. bandit and gitleaks are reasonable
per-call; semgrep is heavier and usually better as a commit-time gate (where
`rein review` already runs it). The named scanners must be installed; a missing
one is skipped.

## How it fits with Strands' own safety

Strands shell relies on up-front **isolation** (declare what the agent can reach;
everything else does not exist), and Strands offers model-level **guardrails**.
`rein-strands` is the complementary **deterministic, code-level** layer: isolation
controls what an action can touch, rein judges what the code itself does, before
it runs. It is intentionally narrow and precise rather than a broad shell-pattern
scanner, so it does not cry wolf on ordinary commands.

## Design

The decision logic (`evaluate`) is a pure function with no Strands or LLM
dependency; `ReinToolGuard` is the thin hook that wires it into an agent, and
`extraction.py` holds the Strands-specific tool-shape mapping. The core has no
dependencies beyond rein. Verified against `strands-agents` 1.44, with an
end-to-end test that drives a real agent.

## License

Apache-2.0.
