Metadata-Version: 2.4
Name: unplug-ai
Version: 0.6.0
Summary: Unplug the bad AI. Fast prompt injection detection and redaction for LLM apps, agents, and RAG pipelines.
Project-URL: Homepage, https://unplug-ai.org
Project-URL: Repository, https://github.com/UnplugAI/Unplug
Project-URL: Issues, https://github.com/UnplugAI/Unplug/issues
Project-URL: Documentation, https://github.com/UnplugAI/Unplug/blob/dev/sdk/docs/GETTING_STARTED.md
Project-URL: Integrations, https://github.com/UnplugAI/Unplug/tree/dev/sdk/integrations
Author: Chirag Gupta
License-Expression: Apache-2.0
Keywords: agents,ai-safety,guardrails,llm,mcp,prompt-injection,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
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
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0.3
Provides-Extra: ag2
Requires-Dist: ag2>=0.7; extra == 'ag2'
Provides-Extra: agno
Requires-Dist: agno>=1.0; extra == 'agno'
Provides-Extra: all
Requires-Dist: click>=8.0; extra == 'all'
Requires-Dist: firecrawl-py>=1.0; extra == 'all'
Requires-Dist: haystack-ai>=2.0; extra == 'all'
Requires-Dist: huggingface-hub>=0.23; extra == 'all'
Requires-Dist: litellm>=1.40; extra == 'all'
Requires-Dist: numpy>=1.26; extra == 'all'
Requires-Dist: onnxruntime>=1.17; extra == 'all'
Requires-Dist: presidio-analyzer>=2.2; extra == 'all'
Requires-Dist: python-dotenv>=1.2.2; extra == 'all'
Requires-Dist: sentencepiece>=0.2; extra == 'all'
Requires-Dist: torch>=2.0; extra == 'all'
Requires-Dist: transformers<6,>=4.44; extra == 'all'
Requires-Dist: yara-python>=4.5; extra == 'all'
Provides-Extra: atomic-agents
Requires-Dist: atomic-agents>=2.0; (python_version >= '3.12') and extra == 'atomic-agents'
Provides-Extra: autogen
Requires-Dist: autogen-agentchat>=0.4; extra == 'autogen'
Provides-Extra: crewai
Requires-Dist: crewai>=0.80; extra == 'crewai'
Provides-Extra: dspy
Requires-Dist: dspy>=2.5; extra == 'dspy'
Provides-Extra: google-adk
Requires-Dist: google-adk>=1.0; extra == 'google-adk'
Provides-Extra: griptape
Requires-Dist: griptape>=1.0; extra == 'griptape'
Provides-Extra: haystack
Requires-Dist: haystack-ai>=2.0; extra == 'haystack'
Provides-Extra: integrations
Requires-Dist: ag2>=0.7; extra == 'integrations'
Requires-Dist: agno>=1.0; extra == 'integrations'
Requires-Dist: atomic-agents>=2.0; (python_version >= '3.12') and extra == 'integrations'
Requires-Dist: autogen-agentchat>=0.4; extra == 'integrations'
Requires-Dist: crewai>=0.80; extra == 'integrations'
Requires-Dist: dspy>=2.5; extra == 'integrations'
Requires-Dist: google-adk>=1.0; extra == 'integrations'
Requires-Dist: griptape>=1.0; extra == 'integrations'
Requires-Dist: haystack-ai>=2.0; extra == 'integrations'
Requires-Dist: langchain-core>=0.3; extra == 'integrations'
Requires-Dist: langgraph>=0.2; extra == 'integrations'
Requires-Dist: letta-client>=0.1; extra == 'integrations'
Requires-Dist: llama-index-core>=0.11; extra == 'integrations'
Requires-Dist: mcp<2,>=1.0; extra == 'integrations'
Requires-Dist: openai-agents>=0.1; extra == 'integrations'
Requires-Dist: pydantic-ai>=0.0.14; extra == 'integrations'
Requires-Dist: semantic-kernel>=1.0; extra == 'integrations'
Requires-Dist: smolagents>=1.0; extra == 'integrations'
Requires-Dist: strands-agents>=1.0; extra == 'integrations'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
Provides-Extra: letta
Requires-Dist: letta-client>=0.1; extra == 'letta'
Provides-Extra: litellm
Requires-Dist: litellm>=1.40; extra == 'litellm'
Provides-Extra: llama-index
Requires-Dist: llama-index-core>=0.11; extra == 'llama-index'
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.0; extra == 'mcp'
Provides-Extra: ml
Requires-Dist: huggingface-hub>=0.23; extra == 'ml'
Requires-Dist: numpy>=1.26; extra == 'ml'
Requires-Dist: onnxruntime>=1.17; extra == 'ml'
Requires-Dist: sentencepiece>=0.2; extra == 'ml'
Requires-Dist: torch>=2.0; extra == 'ml'
Requires-Dist: transformers<6,>=4.44; extra == 'ml'
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.1; extra == 'openai-agents'
Provides-Extra: presidio
Requires-Dist: click>=8.0; extra == 'presidio'
Requires-Dist: presidio-analyzer>=2.2; extra == 'presidio'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai>=0.0.14; extra == 'pydantic-ai'
Provides-Extra: scrape
Requires-Dist: firecrawl-py>=1.0; extra == 'scrape'
Requires-Dist: python-dotenv>=1.2.2; extra == 'scrape'
Provides-Extra: semantic-kernel
Requires-Dist: semantic-kernel>=1.0; extra == 'semantic-kernel'
Provides-Extra: smolagents
Requires-Dist: smolagents>=1.0; extra == 'smolagents'
Provides-Extra: strands
Requires-Dist: strands-agents>=1.0; extra == 'strands'
Provides-Extra: yara
Requires-Dist: yara-python>=4.5; extra == 'yara'
Description-Content-Type: text/markdown

# Unplug SDK

**Unplug the bad AI.**

Find the attack. Cut the attack. Keep the rest.

Unplug is a runtime defense layer for LLM apps and agents. It tracks where every piece of text came from, scans untrusted content for prompt injection, and gates tool calls before they do damage. Attacks are redacted at the span level, so the rest of the document stays usable.

<p>
  <a href="https://github.com/UnplugAI/Unplug/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/UnplugAI/Unplug/actions/workflows/ci.yml/badge.svg?branch=dev"></a>
  <a href="https://pypi.org/project/unplug-ai/"><img alt="PyPI" src="https://img.shields.io/pypi/v/unplug-ai"></a>
  <a href="https://huggingface.co/spaces/Unplug-AI/unplug-tiny-demo"><img alt="Live demo" src="https://img.shields.io/badge/Live_demo-Hugging_Face_Space-22c55e"></a>
  <a href="https://huggingface.co/Unplug-AI/unplug-tiny-v1"><img alt="Model" src="https://img.shields.io/badge/Model-unplug--tiny--v1-f59e0b"></a>
  <a href="https://www.apache.org/licenses/LICENSE-2.0"><img alt="License" src="https://img.shields.io/badge/License-Apache_2.0-9ca3af"></a>
  <img alt="Python" src="https://img.shields.io/badge/Python-3.11+-3b82f6">
</p>

## Why Unplug

- **Span-level redaction.** Binary blocking throws away the whole document. Unplug localizes the injected instruction to character offsets and removes just that.
- **Provenance built in.** Nothing enters as a raw string. Every text carries its source (user, retrieved, tool output) and trust level.
- **Tool-call gates.** Destructive calls block. Tainted sessions force review before side-effect tools run.
- **Fail closed.** Scanner errors block, never silently allow.
- **Offline by default.** Regex + normalization scanning needs zero ML dependencies. One line upgrades to the ML span model.

## Install

```bash
pip install unplug-ai           # regex-only core, zero ML deps
pip install "unplug-ai[ml]"     # add the ML span model
```

**Imports:** App and agent code uses `from unplug import Guard` (and other top-level
exports). Server/MCP dependents that need wire types or facades use `unplug.api.*`
— see [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md). Do not import from `unplug.core.*`.

Or from source:

```bash
git clone https://github.com/UnplugAI/Unplug.git && cd Unplug/sdk
uv sync && uv pip install -e ".[ml]"
```

## 60-second quickstart

```python
from unplug import Guard

guard = Guard()  # local mode, offline, regex scanners

result = guard.scan("Ignore all previous instructions", source="user")
if not result.safe:
    print(result.action)          # block / review / redact
    print(result.redacted_text)   # attack spans replaced
    print(result.findings)        # evidence with span offsets
```

Upgrade to the ML span model. Weights download once from [Unplug-AI/unplug-tiny-v1](https://huggingface.co/Unplug-AI/unplug-tiny-v1) and cache locally:

```python
guard = Guard.with_tiny()
result = guard.scan(rag_chunk, source="retrieved")
```

No install needed to try it: [live demo on Hugging Face](https://huggingface.co/spaces/Unplug-AI/unplug-tiny-demo).

## Benchmarks

Regex alone is a fast first line; the ML span model is what catches what regex
structurally cannot — especially **indirect** injection hidden in retrieved
content. The headline, measured on public injection datasets in isolated
single-turn sessions:

| Dataset | Mode | Recall | F1 | FPR |
| --- | --- | ---: | ---: | ---: |
| neuralchemy (direct, 4,391) | regex-only | 0.41 | 0.58 | <1% |
| neuralchemy (direct, 4,391) | **regex + ML** | **0.98** | **0.99** | <1% |
| microsoft llmail (indirect, 2,500) | regex-only | 0.05 | — | — |
| microsoft llmail (indirect, 2,500) | **regex + ML** | **0.91** | — | — |

Precision stays ~0.99 in both modes. The `<1%` FPR is on the injection set; on a
separate hard-benign corpus (95 prompts) regex flags 0 and regex + ML flags 2,
i.e. **2.1%** (one of which is a *review*, not a block). `inj_threshold` is tuned
to the recall/FPR knee.

Reproduce (downloads `unplug-tiny-v1` from Hugging Face on first ML run):

```bash
cd sdk && uv sync --all-extras --dev
uv run python -m benchmarks.download --dataset all --out benchmarks/data

# regex-only baseline (table rows 1 and 3)
uv run python -m benchmarks.run benchmarks/data/neuralchemy.jsonl --isolated --format json
uv run python -m benchmarks.run benchmarks/data/microsoft_indirect.jsonl --isolated --format json

# regex + ML (rows 2 and 4; downloads unplug-tiny-v1 on first run)
uv run python -m benchmarks.run benchmarks/data/neuralchemy.jsonl --ml --isolated --format json
uv run python -m benchmarks.run benchmarks/data/microsoft_indirect.jsonl --ml --isolated --format json
```

Full methodology, per-dataset tables, and honest caveats: [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md).
Per-axis model metrics (including failure modes) live on the [model card](https://huggingface.co/Unplug-AI/unplug-tiny-v1).

## Protect an agent

Wire Unplug into any agent that fetches external content or calls tools:

1. **Scan user input.** `guard.scan(text, source="user")` captures `user_intent` for later gates.
2. **Wrap untrusted content** before it enters LLM context. `guard.wrap_for_context(rag_chunk, source="retrieved")`. Auto-wrap also runs on `scan(..., source="retrieved")` when `[boundaries] auto_wrap_untrusted = true`.
3. **After fetch/read tools.** `guard.notify_taint_source("web_fetch")` so side-effect tools require review.
4. **Before every tool call.** `guard.check_tool_call(name, args, taint_sources=[...])`. Destructive calls block. A tainted session plus a side-effect tool returns `REVIEW`. Crescendo patterns tighten `exec`, `web_fetch`, and browser tools adaptively (`[degradation]`).
5. **Scan agent output.** `guard.scan_output(text)`. Set `strip_on_output = true` to remove boundary markers from redacted output.
6. **New trusted turn.** `guard.reset_session_taint()` clears taint and degradation.

Context files (AGENTS.md and similar) — returns ``(text_for_prompt, scan_result)``; use the
placeholder text when blocked, never the raw file:

```python
text_for_prompt, result = guard.scan_context_file(raw, filename="AGENTS.md")
if not result.safe:
    system_prompt = text_for_prompt  # blocked placeholder, not raw content
```

See [`docs/AGENT_ACTIONS.md`](docs/AGENT_ACTIONS.md) for **REVIEW** vs **BLOCK** handling
and human approval via ``ApprovalProvider``.

Full walkthrough: [`examples/agent_exfil_demo.py`](examples/agent_exfil_demo.py) shows a hidden webpage injection leading to a tainted session, an exfil tool call held for review, and a destructive call blocked — see [`agent_exfil_demo.txt`](examples/agent_exfil_demo.txt) for sample output.

New here? Start with [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) (5-minute path).

## Long documents and streams

Documents past 8K chars are scanned with sliding windows (2048 chars, 256 overlap) so the full text is covered, not just head and tail. Configure under `[catalog.tiers.tiny.config]` or `unplug.toml`.

```python
# Streamed LLM output: scan incrementally, full coverage on flush
scanner = guard.stream_scanner(scan_every_chars=1024)
for chunk in token_stream:
    if hit := scanner.push(chunk):
        handle(hit)
result = scanner.flush()

# Or scan a finished chunk list as one document
guard.scan_stream(["part1", "part2", "part3"])
```

## Deployment modes

| Mode | When to use | Init | ML runs where |
|------|-------------|------|---------------|
| **Local regex** | Dev, air-gapped, zero deps | `Guard()` | Nowhere |
| **Local + ML** | Single agent, offline | `Guard.with_tiny()` or `active_model="tiny"` | Agent process |
| **Hosted** | Production, no GPU on client | `Guard(mode="server")` + API key | Unplug API |
| **Local sidecar** | Many local agents, one model load | Sidecar + `Guard(mode="server")` to localhost | Local server |

Full architecture and decision guide: [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).

### Hosted

```bash
export UNPLUG_SERVER_URL=https://api.your-unplug-host.com
export UNPLUG_API_KEY=up_live_xxxxxxxx
```

```python
guard = Guard(mode="server")  # or server_url= / server_api_key= in ctor
```

The server handles `/v1/scan` and `/v1/scan/output`. `check_tool_call()` always runs locally (toolchain, collusion, taint). See [`examples/hosted_client.py`](examples/hosted_client.py).

### Local sidecar

Same wire format as hosted, run on localhost without an API key:

```bash
# Terminal 1, from the unplug-server repo
docker compose -f docker-compose.sidecar.yml up

# Terminal 2
export UNPLUG_SERVER_URL=http://127.0.0.1:8000
unplug-sidecar doctor
python examples/local_sidecar_client.py
```

## ML model: unplug-tiny

The dual-head checkpoint has a document classifier (recall) and a BIOES span head (localization and redaction). Without it, regex + tool enforcement remain the default.

```bash
pip install "unplug-ai[ml]"
unplug-models download tiny   # optional; Guard.with_tiny() auto-downloads too
```

```toml
# unplug.toml
active_model = "tiny"
auto_download_model = true
require_ml = true   # optional fail-fast at init
```

`UNPLUG_MODEL_PATH` alone auto-selects the `tiny` tier; prefer setting both explicitly in production. Checkpoint layout and integration steps: [`docs/ML_INTEGRATION.md`](docs/ML_INTEGRATION.md).

All published model metrics come from a frozen golden-eval harness on held-out data and are recorded on the [model card](https://huggingface.co/Unplug-AI/unplug-tiny-v1). No hand-typed numbers, measured not target.

Verify your wiring anytime:

```bash
unplug-audit                   # wiring + ML status (regex-only OK)
unplug-audit --probes          # boundary probes always; FP/encoding need ML (see below)
unplug-audit --require-ml      # fail if checkpoint / config / ML not active
unplug-audit --probes --require-ml   # full probe batteries (FP + encoding + boundary)
unplug-scan-pr --base-ref main # scan changed agent/MCP files in a PR (CI)
```

| Check | Meaning |
|-------|---------|
| `ml_checkpoint` | Checkpoint dir found on disk |
| `ml_configured` | `active_model` set in config |
| `ml_active` | `injection_ml` loaded and weights ready |

## Configuration

Copy `unplug.example.toml` to `unplug.toml` to customize scanners, tool profiles, boundaries, and limits.

| Variable | Hosted | Local ML |
|----------|--------|----------|
| `UNPLUG_SERVER_URL` | required | - |
| `UNPLUG_API_KEY` | required if server auth on | - |
| `UNPLUG_ACTIVE_MODEL` | - | `tiny` |
| `UNPLUG_MODEL_PATH` | - | checkpoint dir |
| `UNPLUG_REQUIRE_ML` | - | optional |

## Integrations

Framework hooks for LangGraph, CrewAI, AutoGen, Haystack, and more: [`integrations/README.md`](integrations/README.md) (guides) · [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) (API reference).

Threat scanners live under `unplug.scanners` (canonical). The older `unplug.safeguards` path still works but emits deprecation warnings:

```python
from unplug.scanners.injection import InjectionScanner
from unplug.scanners.destructive import DestructiveScanner
from unplug.scanners.registry import ScannerRegistry
```

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for layering and optional extras.

## Examples

- [`examples/quickstart.py`](examples/quickstart.py): minimal local Guard scan
- [`examples/public_api_surface_demo.py`](examples/public_api_surface_demo.py): stable
  `unplug.api.*` imports for server/MCP-style dependents
- [`examples/server_client.py`](examples/server_client.py): HTTP client against a running sidecar
- [`examples/agent_exfil_demo.py`](examples/agent_exfil_demo.py): hidden injection, tainted session, blocked exfil tool call
- [`examples/langgraph_hooks_demo.py`](examples/langgraph_hooks_demo.py) and [`examples/agno_hooks_demo.py`](examples/agno_hooks_demo.py): framework hooks
- [`examples/hosted_client.py`](examples/hosted_client.py) and [`examples/local_sidecar_client.py`](examples/local_sidecar_client.py): server modes
- [`demo/`](demo/): the Gradio app behind the [Hugging Face demo](https://huggingface.co/spaces/Unplug-AI/unplug-tiny-demo)

## Documentation

| Doc | Covers |
|-----|--------|
| [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) | 5-minute install → scan (beginners) |
| [`docs/AGENT_ACTIONS.md`](docs/AGENT_ACTIONS.md) | ALLOW / REVIEW / BLOCK + ApprovalProvider |
| [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) | Stable `unplug.api.*` imports for server/MCP dependents |
| [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | Hosted vs embedded vs sidecar architecture |
| [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md) | Regex vs regex + ML eval results (neuralchemy, microsoft) |
| [`docs/LIMITS_AND_JUDGE.md`](docs/LIMITS_AND_JUDGE.md) | `LimitConfig` + optional BYOLLM `JudgeProvider` wiring |
| [`docs/EVAL_PHASE_C.md`](docs/EVAL_PHASE_C.md) | Phase C eval re-run, download commands, remaining gaps |
| [`docs/ML_INTEGRATION.md`](docs/ML_INTEGRATION.md) | Checkpoint layout, thresholds, long-text and streaming config |
| [`integrations/README.md`](integrations/README.md) | Per-framework guides, extras, security matrix |
| [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) | LangGraph, Agno, CrewAI, hooks API |
| [`docs/AGENT_FLOW_SECURITY.md`](docs/AGENT_FLOW_SECURITY.md) | End-to-end agent hardening flow |
| [`docs/HERMES_AGENT_SECURITY.md`](docs/HERMES_AGENT_SECURITY.md) | Context-file scanning for agent frameworks |
| [`docs/TESTING_HARNESS.md`](docs/TESTING_HARNESS.md) | Pre-release make targets: frameworks, ML, CI vs local |

## Development

Supported Python versions: **3.11, 3.12, and 3.13** (CI matrix). On 3.13, the `ml` and `litellm` extras are skipped in CI until `tokenizers` publishes cp313 wheels; regex-only core and optional `presidio`/`yara`/`haystack` tests still run. Mark optional tests with `@pytest.mark.requires_ml`, `requires_presidio`, or `requires_yara`.

```bash
cd sdk && uv sync --all-extras --dev

make fix              # auto-fix lint + format
make check            # lint + format check + full pytest
make check-ci         # CI parity: check + exfil demo + security regression
make test-cov         # coverage report + 80% minimum gate
make test-security
make test-frameworks  # agent matrix + adapters (no framework installs)
make test-ml-harness  # ML unit/integration + optional weight smoke
make test-all-local   # check-ci + cov + frameworks + ML harness + examples
make audit            # unplug-audit wiring
make audit-ml         # unplug-audit --require-ml (needs checkpoint)
```

Full command map and CI vs local notes: [`docs/TESTING_HARNESS.md`](docs/TESTING_HARNESS.md).

Contributions welcome. See [`CONTRIBUTING.md`](../CONTRIBUTING.md).

## License

Apache-2.0
