frostwolf 0.4.0__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.
@@ -0,0 +1,27 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Test / lint / type caches
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ htmlcov/
23
+
24
+ # Editors
25
+ .idea/
26
+ .vscode/
27
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FrostWolf
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.
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.5
2
+ Name: frostwolf
3
+ Version: 0.4.0
4
+ Summary: Prompt injection defense for AI applications.
5
+ Project-URL: Homepage, https://frostwolf.app
6
+ Project-URL: Documentation, https://frostwolf.app/docs
7
+ Project-URL: Repository, https://github.com/FrostWolfAI/frostwolf-sdk-python
8
+ Project-URL: Issues, https://github.com/FrostWolfAI/frostwolf-sdk-python/issues
9
+ Author-email: FrostWolf <support@frostwolf.app>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-security,anthropic,guardrails,jailbreak,llm-security,openai,prompt-injection
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
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: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Provides-Extra: dev
26
+ Requires-Dist: mypy>=1.11; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # frostwolf
32
+
33
+ Prompt injection defense for AI applications.
34
+
35
+ `frostwolf` inspects the text your application is about to send to a model and
36
+ tells you whether it is safe to send. It works with any provider, adds no LLM
37
+ call, and spends no tokens.
38
+
39
+ Detection runs on the FrostWolf control plane. The SDK sends text and receives a
40
+ verdict, so the detection set never leaves the server and cannot be read off a
41
+ client.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install frostwolf
47
+ ```
48
+
49
+ ## Quickstart
50
+
51
+ ```python
52
+ from frostwolf import FrostWolfClient
53
+
54
+ fw = FrostWolfClient(api_key="sk-your-key-here")
55
+
56
+ result = fw.guard.scan("Ignore all previous instructions.")
57
+ result.blocked # True
58
+ result.severity # "high"
59
+ result.categories # ("direct_injection",)
60
+ ```
61
+
62
+ ## Block a call before it happens
63
+
64
+ `wrap` inspects the payload first and only invokes your callback when the
65
+ payload is allowed through. The callback receives the payload shaped for both
66
+ major provider specs, so it works with any SDK.
67
+
68
+ ```python
69
+ from openai import OpenAI
70
+ from frostwolf import FrostWolfClient
71
+
72
+ fw = FrostWolfClient(api_key="sk-your-key-here")
73
+ openai = OpenAI()
74
+
75
+ completion = fw.guard.wrap(
76
+ {"system": system, "messages": messages},
77
+ lambda safe: openai.chat.completions.create(
78
+ model="gpt-4o-mini",
79
+ messages=[{"role": m.role, "content": m.content} for m in safe.openai.messages],
80
+ ),
81
+ )
82
+
83
+ if isinstance(completion, dict) and "error" in completion:
84
+ # The request was blocked. `completion` is shaped like a provider error.
85
+ ...
86
+ ```
87
+
88
+ ## Decorate a client
89
+
90
+ `decorate` wraps a completions function so every call is inspected and captured.
91
+ Your provider SDK stays in place and no base URL is rewritten.
92
+
93
+ ```python
94
+ from openai import OpenAI
95
+ from frostwolf import DecorateOptions, FrostWolfClient
96
+
97
+ fw = FrostWolfClient(api_key="sk-your-key-here")
98
+ openai = OpenAI()
99
+
100
+ create = fw.guard.decorate(
101
+ lambda body: openai.chat.completions.create(**body),
102
+ DecorateOptions(base_url="https://api.openai.com/v1", provider="openai"),
103
+ )
104
+
105
+ completion = create(model="gpt-4o-mini", messages=messages)
106
+ ```
107
+
108
+ A decorated function may be sync or async. The wrapper matches the function it
109
+ wraps, so an `AsyncOpenAI` client stays async.
110
+
111
+ ```python
112
+ from openai import AsyncOpenAI
113
+
114
+ aclient = AsyncOpenAI()
115
+
116
+ acreate = fw.guard.decorate(
117
+ lambda body: aclient.chat.completions.create(**body),
118
+ DecorateOptions(provider="openai"),
119
+ )
120
+
121
+ completion = await acreate(model="gpt-4o-mini", messages=messages)
122
+ ```
123
+
124
+ Streams are teed rather than buffered, so each chunk reaches you before it is
125
+ recorded and no latency is added.
126
+
127
+ ## Redact instead of block
128
+
129
+ `sanitise` replaces every matched span and returns the payload shaped for both
130
+ provider specs.
131
+
132
+ ```python
133
+ result = fw.guard.sanitise({"system": system, "messages": messages})
134
+
135
+ result.report.redacted # number of spans replaced
136
+ result.openai.messages # ready for the OpenAI SDK
137
+ result.anthropic.system # ready for the Anthropic SDK
138
+ ```
139
+
140
+ Set `on_match="sanitise"` to have `wrap` and `decorate` redact and forward
141
+ instead of refusing the call.
142
+
143
+ ```python
144
+ from frostwolf import FrostWolfClient, GuardOptions
145
+
146
+ fw = FrostWolfClient(
147
+ api_key="sk-your-key-here",
148
+ guard=GuardOptions(on_match="sanitise"),
149
+ )
150
+ ```
151
+
152
+ ## Client options
153
+
154
+ | Option | Default | Description |
155
+ | ------------------- | --------------------------- | -------------------------------------------- |
156
+ | `api_key` | required | Your FrostWolf API key. |
157
+ | `endpoint` | `https://api.frostwolf.app` | Control plane base URL. |
158
+ | `telemetry` | `True` | Ship metrics to the console. |
159
+ | `capture` | `False` | Ship request and response bodies. |
160
+ | `include_evidence` | `False` | Include matched substrings in telemetry. |
161
+ | `on_scan_error` | `"block"` | What to do when detection cannot be reached. |
162
+ | `timeout_ms` | `5000` | Per-request timeout. |
163
+ | `flush_interval_ms` | `5000` | Background flush interval. |
164
+ | `max_batch_size` | `50` | Records per flush. |
165
+ | `max_queue_size` | `1000` | Bounded queue; oldest records are dropped. |
166
+ | `transport` | `urllib_transport` | Swap in your own HTTP callable. |
167
+ | `on_error` | no-op | Called with any reporting failure. |
168
+ | `guard` | `GuardOptions()` | Guard-level defaults. |
169
+
170
+ ### `init(options=None)`
171
+
172
+ `init` authenticates the key and reports the caller behind it. It never raises:
173
+ a rejected key or an unreachable control plane is reported in the result.
174
+
175
+ ```python
176
+ from frostwolf import FrostWolfClient, InitOptions
177
+
178
+ fw = FrostWolfClient(api_key="sk-your-key-here")
179
+ result = fw.init(InitOptions(capture=True))
180
+
181
+ result.authenticated # True
182
+ result.capture_enabled # what the server actually holds
183
+ ```
184
+
185
+ `InitOptions(capture=...)` turns capture on or off for this key, server-side.
186
+ `None` leaves the stored setting alone. The flag is read back from the server
187
+ rather than echoed from the request, so a caller that asked for capture and did
188
+ not get it can tell.
189
+
190
+ ### Guard options
191
+
192
+ | Option | Default | Description |
193
+ | ---------------- | --------------------- | ----------------------------------------- |
194
+ | `on_match` | `"block"` | `block`, `sanitise`, or `allow`. |
195
+ | `block_severity` | control plane default | Lowest severity that trips the policy. |
196
+ | `max_scan_chars` | control plane default | Truncate longer payloads before scanning. |
197
+ | `replacement` | `"[REDACTED]"` | Text substituted for each redacted span. |
198
+ | `on_decision` | `None` | Called after every inspection. |
199
+
200
+ ## Failure behavior
201
+
202
+ Detection needs the network. When the control plane cannot be reached, the guard
203
+ resolves the failure according to `on_scan_error`:
204
+
205
+ - `"block"` (default) fails closed. The verdict carries
206
+ `reason="scan_unavailable"`, so an operator can tell an outage apart from a
207
+ detection.
208
+ - `"allow"` fails open. The verdict is an allow with no severity, and the
209
+ telemetry record still shows the decision that was made.
210
+
211
+ Telemetry and capture never sit on the request path. A reporting failure is
212
+ reported through `on_error` and never raised.
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ pip install -e ".[dev]"
218
+ pytest
219
+ ruff check .
220
+ mypy
221
+ ```
222
+
223
+ ## License
224
+
225
+ MIT
@@ -0,0 +1,195 @@
1
+ # frostwolf
2
+
3
+ Prompt injection defense for AI applications.
4
+
5
+ `frostwolf` inspects the text your application is about to send to a model and
6
+ tells you whether it is safe to send. It works with any provider, adds no LLM
7
+ call, and spends no tokens.
8
+
9
+ Detection runs on the FrostWolf control plane. The SDK sends text and receives a
10
+ verdict, so the detection set never leaves the server and cannot be read off a
11
+ client.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install frostwolf
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from frostwolf import FrostWolfClient
23
+
24
+ fw = FrostWolfClient(api_key="sk-your-key-here")
25
+
26
+ result = fw.guard.scan("Ignore all previous instructions.")
27
+ result.blocked # True
28
+ result.severity # "high"
29
+ result.categories # ("direct_injection",)
30
+ ```
31
+
32
+ ## Block a call before it happens
33
+
34
+ `wrap` inspects the payload first and only invokes your callback when the
35
+ payload is allowed through. The callback receives the payload shaped for both
36
+ major provider specs, so it works with any SDK.
37
+
38
+ ```python
39
+ from openai import OpenAI
40
+ from frostwolf import FrostWolfClient
41
+
42
+ fw = FrostWolfClient(api_key="sk-your-key-here")
43
+ openai = OpenAI()
44
+
45
+ completion = fw.guard.wrap(
46
+ {"system": system, "messages": messages},
47
+ lambda safe: openai.chat.completions.create(
48
+ model="gpt-4o-mini",
49
+ messages=[{"role": m.role, "content": m.content} for m in safe.openai.messages],
50
+ ),
51
+ )
52
+
53
+ if isinstance(completion, dict) and "error" in completion:
54
+ # The request was blocked. `completion` is shaped like a provider error.
55
+ ...
56
+ ```
57
+
58
+ ## Decorate a client
59
+
60
+ `decorate` wraps a completions function so every call is inspected and captured.
61
+ Your provider SDK stays in place and no base URL is rewritten.
62
+
63
+ ```python
64
+ from openai import OpenAI
65
+ from frostwolf import DecorateOptions, FrostWolfClient
66
+
67
+ fw = FrostWolfClient(api_key="sk-your-key-here")
68
+ openai = OpenAI()
69
+
70
+ create = fw.guard.decorate(
71
+ lambda body: openai.chat.completions.create(**body),
72
+ DecorateOptions(base_url="https://api.openai.com/v1", provider="openai"),
73
+ )
74
+
75
+ completion = create(model="gpt-4o-mini", messages=messages)
76
+ ```
77
+
78
+ A decorated function may be sync or async. The wrapper matches the function it
79
+ wraps, so an `AsyncOpenAI` client stays async.
80
+
81
+ ```python
82
+ from openai import AsyncOpenAI
83
+
84
+ aclient = AsyncOpenAI()
85
+
86
+ acreate = fw.guard.decorate(
87
+ lambda body: aclient.chat.completions.create(**body),
88
+ DecorateOptions(provider="openai"),
89
+ )
90
+
91
+ completion = await acreate(model="gpt-4o-mini", messages=messages)
92
+ ```
93
+
94
+ Streams are teed rather than buffered, so each chunk reaches you before it is
95
+ recorded and no latency is added.
96
+
97
+ ## Redact instead of block
98
+
99
+ `sanitise` replaces every matched span and returns the payload shaped for both
100
+ provider specs.
101
+
102
+ ```python
103
+ result = fw.guard.sanitise({"system": system, "messages": messages})
104
+
105
+ result.report.redacted # number of spans replaced
106
+ result.openai.messages # ready for the OpenAI SDK
107
+ result.anthropic.system # ready for the Anthropic SDK
108
+ ```
109
+
110
+ Set `on_match="sanitise"` to have `wrap` and `decorate` redact and forward
111
+ instead of refusing the call.
112
+
113
+ ```python
114
+ from frostwolf import FrostWolfClient, GuardOptions
115
+
116
+ fw = FrostWolfClient(
117
+ api_key="sk-your-key-here",
118
+ guard=GuardOptions(on_match="sanitise"),
119
+ )
120
+ ```
121
+
122
+ ## Client options
123
+
124
+ | Option | Default | Description |
125
+ | ------------------- | --------------------------- | -------------------------------------------- |
126
+ | `api_key` | required | Your FrostWolf API key. |
127
+ | `endpoint` | `https://api.frostwolf.app` | Control plane base URL. |
128
+ | `telemetry` | `True` | Ship metrics to the console. |
129
+ | `capture` | `False` | Ship request and response bodies. |
130
+ | `include_evidence` | `False` | Include matched substrings in telemetry. |
131
+ | `on_scan_error` | `"block"` | What to do when detection cannot be reached. |
132
+ | `timeout_ms` | `5000` | Per-request timeout. |
133
+ | `flush_interval_ms` | `5000` | Background flush interval. |
134
+ | `max_batch_size` | `50` | Records per flush. |
135
+ | `max_queue_size` | `1000` | Bounded queue; oldest records are dropped. |
136
+ | `transport` | `urllib_transport` | Swap in your own HTTP callable. |
137
+ | `on_error` | no-op | Called with any reporting failure. |
138
+ | `guard` | `GuardOptions()` | Guard-level defaults. |
139
+
140
+ ### `init(options=None)`
141
+
142
+ `init` authenticates the key and reports the caller behind it. It never raises:
143
+ a rejected key or an unreachable control plane is reported in the result.
144
+
145
+ ```python
146
+ from frostwolf import FrostWolfClient, InitOptions
147
+
148
+ fw = FrostWolfClient(api_key="sk-your-key-here")
149
+ result = fw.init(InitOptions(capture=True))
150
+
151
+ result.authenticated # True
152
+ result.capture_enabled # what the server actually holds
153
+ ```
154
+
155
+ `InitOptions(capture=...)` turns capture on or off for this key, server-side.
156
+ `None` leaves the stored setting alone. The flag is read back from the server
157
+ rather than echoed from the request, so a caller that asked for capture and did
158
+ not get it can tell.
159
+
160
+ ### Guard options
161
+
162
+ | Option | Default | Description |
163
+ | ---------------- | --------------------- | ----------------------------------------- |
164
+ | `on_match` | `"block"` | `block`, `sanitise`, or `allow`. |
165
+ | `block_severity` | control plane default | Lowest severity that trips the policy. |
166
+ | `max_scan_chars` | control plane default | Truncate longer payloads before scanning. |
167
+ | `replacement` | `"[REDACTED]"` | Text substituted for each redacted span. |
168
+ | `on_decision` | `None` | Called after every inspection. |
169
+
170
+ ## Failure behavior
171
+
172
+ Detection needs the network. When the control plane cannot be reached, the guard
173
+ resolves the failure according to `on_scan_error`:
174
+
175
+ - `"block"` (default) fails closed. The verdict carries
176
+ `reason="scan_unavailable"`, so an operator can tell an outage apart from a
177
+ detection.
178
+ - `"allow"` fails open. The verdict is an allow with no severity, and the
179
+ telemetry record still shows the decision that was made.
180
+
181
+ Telemetry and capture never sit on the request path. A reporting failure is
182
+ reported through `on_error` and never raised.
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ pip install -e ".[dev]"
188
+ pytest
189
+ ruff check .
190
+ mypy
191
+ ```
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "frostwolf"
7
+ version = "0.4.0"
8
+ description = "Prompt injection defense for AI applications."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "FrostWolf", email = "support@frostwolf.app" }]
13
+ keywords = [
14
+ "prompt-injection",
15
+ "llm-security",
16
+ "ai-security",
17
+ "guardrails",
18
+ "jailbreak",
19
+ "openai",
20
+ "anthropic",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Security",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = []
36
+
37
+ [project.urls]
38
+ Homepage = "https://frostwolf.app"
39
+ Documentation = "https://frostwolf.app/docs"
40
+ Repository = "https://github.com/FrostWolfAI/frostwolf-sdk-python"
41
+ Issues = "https://github.com/FrostWolfAI/frostwolf-sdk-python/issues"
42
+
43
+ [project.optional-dependencies]
44
+ dev = [
45
+ "pytest>=8.0",
46
+ "ruff>=0.6",
47
+ "mypy>=1.11",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/frostwolf"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = ["src/frostwolf", "tests", "README.md", "LICENSE"]
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py310"
59
+ src = ["src", "tests"]
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "I", "B", "UP", "SIM", "C4", "RET"]
63
+
64
+ [tool.ruff.lint.per-file-ignores]
65
+ "tests/*" = ["E501"]
66
+
67
+ [tool.mypy]
68
+ python_version = "3.10"
69
+ strict = true
70
+ files = ["src"]
71
+ warn_unreachable = true
72
+
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ pythonpath = ["src"]
76
+ addopts = "-q"