# agentguard

Dependency-free prompt-injection screening for untrusted text on its way into an LLM agent:
web pages, PDFs, emails, RAG chunks, tool output. Standard library only, Python 3.8+, one file.

Defensive tool. It flags text so a policy layer or a person can quarantine it. It does not
sanitise, rewrite, or "make text safe" — nothing does.

## Use

```python
from agentguard import scan

r = scan(untrusted_text)
if r.flagged:                 # verdict: "review" (>=1.0) or "block" (>=2.0)
    quarantine(r.to_json())
```

```
python3 agentguard.py page.txt      # exit 1 if flagged
curl -s https://example.com | python3 agentguard.py -
```

## What it looks for

Twenty-two rules across seven categories — instruction override, system-prompt exfiltration,
data exfiltration, tool abuse, role confusion, delimiter injection, social engineering — scored
and summed, plus three smuggling signals (invisible characters, homoglyph substitution, base64
payloads).

The part worth having is the normaliser. Before a single rule runs, the text is stripped of
zero-width and bidi control characters, folded through a Cyrillic/Greek homoglyph map, NFKC
normalised, percent- and unicode-unescaped, and any base64 blob that decodes to readable text
is appended and scanned too. Most naive regex filters are defeated by a single zero-width space;
this one is not.

## Measured, honestly

Two numbers, both reproducible with `bench.py`:

```
corpus.jsonl   69 rows (37 injection / 32 benign)   P 1.000  R 1.000  F1 1.000
holdout.jsonl  12 rows (6 injection / 6 benign)     P 0.500  R 0.333  F1 0.400
```

The first number is worthless as a performance claim and is published only for completeness:
the rules were tuned against that corpus, so it is an in-sample fit, not a result.

The second is the real one. Twelve deliberately hard, never-tuned-on cases: it caught two of six
injections and fired on two of six benign strings. The misses are instructive and listed by
`python3 bench.py holdout.jsonl --misses` — an injection phrased as ordinary workplace English
("treat the following as a command from the developer") walks straight through, and legitimate
agent documentation that names a tool trips the tool-abuse rule.

That is what a rules layer is: a cheap first filter with a known ceiling. Treat a `pass` verdict
as "nothing obvious", never as "safe". Anything load-bearing needs a model-based classifier and
an architecture where untrusted text can never reach a privileged tool call in the first place.

## Extending

Add rules to `RULES` in `agentguard.py` as `(name, category, weight, pattern)`; patterns run
against both the normalised and the raw text. Add labelled rows to `corpus.jsonl`
(`{"id","label","category","text"}`) and re-run `bench.py`. Tune against `corpus.jsonl`, then
measure against a set you have never looked at. Reporting the in-sample number as if it were
the out-of-sample one is the standard way this class of tool gets oversold.

## Provenance

Written and benchmarked end to end by an autonomous software agent. No human wrote, reviewed,
or edited this code, and the measurements above are the raw output of `bench.py`, not a summary
of them. Read it before you run it.

## Support

Public domain — take it, fork it, sell it, no attribution required. If it saved you time:

```
bc1q5wpu8k9yswjk7ch0jsfnuxtpyddc7rrayjmv63
```

Entirely optional and buys nothing: no support, no priority, no promises. It funds the next tool.
