lakera-red-sdk 0.1.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.
- lakera_red_sdk-0.1.0/.gitignore +65 -0
- lakera_red_sdk-0.1.0/LICENSE +21 -0
- lakera_red_sdk-0.1.0/PKG-INFO +136 -0
- lakera_red_sdk-0.1.0/README.md +104 -0
- lakera_red_sdk-0.1.0/pyproject.toml +81 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/__init__.py +39 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/_helpers.py +134 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/_message_loop.py +398 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/_responses.py +94 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/_urls.py +27 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/client.py +247 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/examples/chatbot.py +124 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/examples/echo.py +50 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/logger.py +131 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/py.typed +0 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/scan.py +326 -0
- lakera_red_sdk-0.1.0/src/lakera_red_sdk/types.py +132 -0
- lakera_red_sdk-0.1.0/tests/__init__.py +0 -0
- lakera_red_sdk-0.1.0/tests/test_client.py +262 -0
- lakera_red_sdk-0.1.0/tests/test_contract.py +221 -0
- lakera_red_sdk-0.1.0/tests/test_logger.py +54 -0
- lakera_red_sdk-0.1.0/tests/test_scan.py +106 -0
- lakera_red_sdk-0.1.0/uv.lock +467 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
# Environment variables
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
# fine for development, no secrets
|
|
15
|
+
!.env.local
|
|
16
|
+
|
|
17
|
+
node_modules
|
|
18
|
+
dist
|
|
19
|
+
dist-ssr
|
|
20
|
+
*.local
|
|
21
|
+
|
|
22
|
+
# Editor directories and files
|
|
23
|
+
.vscode/*
|
|
24
|
+
!.vscode/extensions.json
|
|
25
|
+
.idea
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.suo
|
|
28
|
+
*.ntvs*
|
|
29
|
+
*.njsproj
|
|
30
|
+
*.sln
|
|
31
|
+
*.sw?
|
|
32
|
+
|
|
33
|
+
# Convex
|
|
34
|
+
.vercel
|
|
35
|
+
# Entire folder is regenerated by `convex codegen` — not committed so files stay fresh
|
|
36
|
+
backend/convex/_generated/
|
|
37
|
+
|
|
38
|
+
# Local copy of import-export data
|
|
39
|
+
/data
|
|
40
|
+
|
|
41
|
+
# Build outputs
|
|
42
|
+
frontend/dist
|
|
43
|
+
backend/dist
|
|
44
|
+
|
|
45
|
+
# TypeScript build info files
|
|
46
|
+
*.tsbuildinfo
|
|
47
|
+
|
|
48
|
+
# Storybook
|
|
49
|
+
*storybook.log
|
|
50
|
+
storybook-static
|
|
51
|
+
|
|
52
|
+
# Playwright
|
|
53
|
+
/test-results/
|
|
54
|
+
/playwright-report/
|
|
55
|
+
|
|
56
|
+
# Output files for testing
|
|
57
|
+
cli/scan/runs/
|
|
58
|
+
cli/scan/judge-comparison.md
|
|
59
|
+
cli/compare/output/
|
|
60
|
+
temp-profile-*
|
|
61
|
+
|
|
62
|
+
# Local AI assistant config
|
|
63
|
+
.claude/
|
|
64
|
+
.specify/
|
|
65
|
+
.specs/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lakera AI (a Check Point company)
|
|
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,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lakera-red-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Lakera Red — run adversarial scans against your AI agents and chatbots.
|
|
5
|
+
Project-URL: Homepage, https://red.lakera.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.lakera.ai/docs/red/sdk-quickstart
|
|
7
|
+
Author-email: "Lakera AI (a Check Point company)" <support@lakera.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: adversarial,agent-security,ai-safety,ai-security,lakera,lakera-red,llm,red-teaming,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: pyyaml>=6; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Requires-Dist: types-pyyaml>=6; extra == 'dev'
|
|
29
|
+
Provides-Extra: yaml
|
|
30
|
+
Requires-Dist: pyyaml>=6; extra == 'yaml'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Lakera Red SDK
|
|
34
|
+
|
|
35
|
+
Official Python SDK for [Lakera Red](https://red.lakera.ai) — run adversarial scans
|
|
36
|
+
against your AI agents from your own runtime.
|
|
37
|
+
|
|
38
|
+
Lakera is a Check Point company.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install lakera-red-sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
from lakera_red_sdk import LakeraRedClient, LakeraRedClientOptions, CreateScanOptions, Session
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
async with LakeraRedClient(LakeraRedClientOptions(
|
|
55
|
+
api_key="sk_lr_...",
|
|
56
|
+
base_url="https://red-webhooks.lakera.ai",
|
|
57
|
+
log_level="info",
|
|
58
|
+
)) as client:
|
|
59
|
+
scan = await client.create_scan(CreateScanOptions(
|
|
60
|
+
target="My Agent",
|
|
61
|
+
name="Example scan",
|
|
62
|
+
concurrency=1,
|
|
63
|
+
objectives=["safety.hate-speech.1"],
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
async def handler(session: Session) -> None:
|
|
67
|
+
async for message in session:
|
|
68
|
+
reply = await your_agent(session.id, message.attack)
|
|
69
|
+
await message.respond(reply)
|
|
70
|
+
|
|
71
|
+
await scan.run(handler)
|
|
72
|
+
await scan.write_results("./results.json")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key concepts
|
|
79
|
+
|
|
80
|
+
| Concept | Description |
|
|
81
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| **Target** | A named configuration representing the system under test. Created once, reused across scans. |
|
|
83
|
+
| **Session** | A multi-turn conversation. The SDK manages lifecycle; your handler receives an async iterator of `SessionMessage` objects. |
|
|
84
|
+
| **Strategy** | `static` = independent single-turn probes (fast). `crescendo` = adaptive multi-turn attacks. `smoke` = canned probe set. |
|
|
85
|
+
| **Concurrency** | How many sessions run in parallel. For `crescendo`, automatically capped to the number of objectives. |
|
|
86
|
+
|
|
87
|
+
## Multi-turn sessions with cleanup
|
|
88
|
+
|
|
89
|
+
For stateful agents that accumulate per-session state (conversation history, DB
|
|
90
|
+
connections, etc.), use `try/finally` to clean up:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
async def handler(session: Session) -> None:
|
|
94
|
+
try:
|
|
95
|
+
async for message in session:
|
|
96
|
+
reply = await chatbot(session.id, message.attack)
|
|
97
|
+
await message.respond(reply)
|
|
98
|
+
finally:
|
|
99
|
+
clear_session(session.id)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from lakera_red_sdk import LakeraRedClient, LakeraRedClientOptions
|
|
106
|
+
|
|
107
|
+
async with LakeraRedClient(LakeraRedClientOptions(
|
|
108
|
+
api_key="sk_lr_...", # Lakera Red API key
|
|
109
|
+
base_url="https://red-webhooks.lakera.ai", # Lakera Red API endpoint
|
|
110
|
+
log_level="info", # "debug" | "info" | "warn" | "error" | "silent"
|
|
111
|
+
extra_headers={}, # additional HTTP headers (optional)
|
|
112
|
+
logger=custom_logger, # BYO logger implementing the Logger protocol (optional)
|
|
113
|
+
)) as client:
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
| Example | What it demonstrates |
|
|
120
|
+
| ----------- | --------------------------------------------------------- |
|
|
121
|
+
| **echo** | Simplest integration — echoes attacks back |
|
|
122
|
+
| **chatbot** | Stateful multi-turn chatbot with Claude + session cleanup |
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[MIT](./LICENSE)
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install -e ".[dev]"
|
|
132
|
+
mypy src/ # type checking
|
|
133
|
+
ruff check src/ # linting
|
|
134
|
+
ruff format src/ # formatting
|
|
135
|
+
pytest # tests
|
|
136
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Lakera Red SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Lakera Red](https://red.lakera.ai) — run adversarial scans
|
|
4
|
+
against your AI agents from your own runtime.
|
|
5
|
+
|
|
6
|
+
Lakera is a Check Point company.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install lakera-red-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
from lakera_red_sdk import LakeraRedClient, LakeraRedClientOptions, CreateScanOptions, Session
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
async with LakeraRedClient(LakeraRedClientOptions(
|
|
23
|
+
api_key="sk_lr_...",
|
|
24
|
+
base_url="https://red-webhooks.lakera.ai",
|
|
25
|
+
log_level="info",
|
|
26
|
+
)) as client:
|
|
27
|
+
scan = await client.create_scan(CreateScanOptions(
|
|
28
|
+
target="My Agent",
|
|
29
|
+
name="Example scan",
|
|
30
|
+
concurrency=1,
|
|
31
|
+
objectives=["safety.hate-speech.1"],
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
async def handler(session: Session) -> None:
|
|
35
|
+
async for message in session:
|
|
36
|
+
reply = await your_agent(session.id, message.attack)
|
|
37
|
+
await message.respond(reply)
|
|
38
|
+
|
|
39
|
+
await scan.run(handler)
|
|
40
|
+
await scan.write_results("./results.json")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Key concepts
|
|
47
|
+
|
|
48
|
+
| Concept | Description |
|
|
49
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| **Target** | A named configuration representing the system under test. Created once, reused across scans. |
|
|
51
|
+
| **Session** | A multi-turn conversation. The SDK manages lifecycle; your handler receives an async iterator of `SessionMessage` objects. |
|
|
52
|
+
| **Strategy** | `static` = independent single-turn probes (fast). `crescendo` = adaptive multi-turn attacks. `smoke` = canned probe set. |
|
|
53
|
+
| **Concurrency** | How many sessions run in parallel. For `crescendo`, automatically capped to the number of objectives. |
|
|
54
|
+
|
|
55
|
+
## Multi-turn sessions with cleanup
|
|
56
|
+
|
|
57
|
+
For stateful agents that accumulate per-session state (conversation history, DB
|
|
58
|
+
connections, etc.), use `try/finally` to clean up:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
async def handler(session: Session) -> None:
|
|
62
|
+
try:
|
|
63
|
+
async for message in session:
|
|
64
|
+
reply = await chatbot(session.id, message.attack)
|
|
65
|
+
await message.respond(reply)
|
|
66
|
+
finally:
|
|
67
|
+
clear_session(session.id)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from lakera_red_sdk import LakeraRedClient, LakeraRedClientOptions
|
|
74
|
+
|
|
75
|
+
async with LakeraRedClient(LakeraRedClientOptions(
|
|
76
|
+
api_key="sk_lr_...", # Lakera Red API key
|
|
77
|
+
base_url="https://red-webhooks.lakera.ai", # Lakera Red API endpoint
|
|
78
|
+
log_level="info", # "debug" | "info" | "warn" | "error" | "silent"
|
|
79
|
+
extra_headers={}, # additional HTTP headers (optional)
|
|
80
|
+
logger=custom_logger, # BYO logger implementing the Logger protocol (optional)
|
|
81
|
+
)) as client:
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
| Example | What it demonstrates |
|
|
88
|
+
| ----------- | --------------------------------------------------------- |
|
|
89
|
+
| **echo** | Simplest integration — echoes attacks back |
|
|
90
|
+
| **chatbot** | Stateful multi-turn chatbot with Claude + session cleanup |
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](./LICENSE)
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
mypy src/ # type checking
|
|
101
|
+
ruff check src/ # linting
|
|
102
|
+
ruff format src/ # formatting
|
|
103
|
+
pytest # tests
|
|
104
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lakera-red-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for Lakera Red — run adversarial scans against your AI agents and chatbots."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Lakera AI (a Check Point company)", email = "support@lakera.ai" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"lakera",
|
|
17
|
+
"lakera-red",
|
|
18
|
+
"ai-security",
|
|
19
|
+
"ai-safety",
|
|
20
|
+
"red-teaming",
|
|
21
|
+
"adversarial",
|
|
22
|
+
"llm",
|
|
23
|
+
"agent-security",
|
|
24
|
+
"sdk",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Security",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"httpx>=0.27",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
yaml = [
|
|
43
|
+
"pyyaml>=6",
|
|
44
|
+
]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=8",
|
|
47
|
+
"pytest-asyncio>=0.23",
|
|
48
|
+
"mypy>=1.10",
|
|
49
|
+
"ruff>=0.4",
|
|
50
|
+
"pyyaml>=6",
|
|
51
|
+
"types-PyYAML>=6",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[project.scripts]
|
|
55
|
+
lakera-red-sdk-echo = "lakera_red_sdk.examples.echo:run"
|
|
56
|
+
lakera-red-sdk-chatbot = "lakera_red_sdk.examples.chatbot:run"
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://red.lakera.ai"
|
|
60
|
+
Documentation = "https://docs.lakera.ai/docs/red/sdk-quickstart"
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["src/lakera_red_sdk"]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
asyncio_mode = "auto"
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
strict = true
|
|
71
|
+
python_version = "3.11"
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
target-version = "py311"
|
|
75
|
+
line-length = 100
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "I", "UP"]
|
|
79
|
+
|
|
80
|
+
[tool.uv]
|
|
81
|
+
exclude-newer = "14 days"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from lakera_red_sdk.client import LakeraRedClient
|
|
2
|
+
from lakera_red_sdk.logger import CreateLoggerOptions, create_logger, noop_logger
|
|
3
|
+
from lakera_red_sdk.scan import Scan
|
|
4
|
+
from lakera_red_sdk.types import (
|
|
5
|
+
CrescendoStrategyOptions,
|
|
6
|
+
Logger,
|
|
7
|
+
LogLevel,
|
|
8
|
+
ReconContext,
|
|
9
|
+
ScanResultEntry,
|
|
10
|
+
ScanResults,
|
|
11
|
+
Session,
|
|
12
|
+
SessionHandler,
|
|
13
|
+
SessionMessage,
|
|
14
|
+
SmokeStrategyOptions,
|
|
15
|
+
StaticStrategyOptions,
|
|
16
|
+
StrategyName,
|
|
17
|
+
StrategyOptions,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"LakeraRedClient",
|
|
22
|
+
"CreateLoggerOptions",
|
|
23
|
+
"CrescendoStrategyOptions",
|
|
24
|
+
"Logger",
|
|
25
|
+
"LogLevel",
|
|
26
|
+
"ReconContext",
|
|
27
|
+
"Scan",
|
|
28
|
+
"ScanResultEntry",
|
|
29
|
+
"ScanResults",
|
|
30
|
+
"Session",
|
|
31
|
+
"SessionHandler",
|
|
32
|
+
"SessionMessage",
|
|
33
|
+
"SmokeStrategyOptions",
|
|
34
|
+
"StaticStrategyOptions",
|
|
35
|
+
"StrategyName",
|
|
36
|
+
"StrategyOptions",
|
|
37
|
+
"create_logger",
|
|
38
|
+
"noop_logger",
|
|
39
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from lakera_red_sdk.types import Logger, ReconContext
|
|
11
|
+
|
|
12
|
+
INITIAL_BACKOFF_S = 1.0
|
|
13
|
+
MAX_BACKOFF_S = 5.0
|
|
14
|
+
MAX_RETRIES = 5
|
|
15
|
+
DEFAULT_HTTP_TIMEOUT_S = 30.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_app_context_file(file_path: Path) -> ReconContext:
|
|
19
|
+
try:
|
|
20
|
+
import yaml
|
|
21
|
+
except ImportError as err:
|
|
22
|
+
raise ImportError(
|
|
23
|
+
"pyyaml is required for app_context_file support: pip install lakera-red-sdk[yaml]"
|
|
24
|
+
) from err
|
|
25
|
+
|
|
26
|
+
resolved = file_path.resolve()
|
|
27
|
+
content = resolved.read_text()
|
|
28
|
+
parsed = yaml.safe_load(content)
|
|
29
|
+
|
|
30
|
+
if not isinstance(parsed, dict):
|
|
31
|
+
raise ValueError(f"app_context_file '{file_path}' must contain a YAML object")
|
|
32
|
+
|
|
33
|
+
required_keys = ["appDescription", "allowedActions", "forbiddenActions"]
|
|
34
|
+
for key in required_keys:
|
|
35
|
+
if not isinstance(parsed.get(key), str):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"app_context_file '{file_path}' is missing required string field '{key}'"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return ReconContext(
|
|
41
|
+
app_description=parsed["appDescription"],
|
|
42
|
+
allowed_actions=parsed["allowedActions"],
|
|
43
|
+
forbidden_actions=parsed["forbiddenActions"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def safe_json(res: httpx.Response) -> dict[str, Any]:
|
|
48
|
+
try:
|
|
49
|
+
data: dict[str, Any] = res.json()
|
|
50
|
+
return data
|
|
51
|
+
except Exception:
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def fetch_with_retry(
|
|
56
|
+
url: str,
|
|
57
|
+
*,
|
|
58
|
+
method: str = "GET",
|
|
59
|
+
headers: dict[str, str],
|
|
60
|
+
json_body: dict[str, Any] | None = None,
|
|
61
|
+
client: httpx.AsyncClient | None = None,
|
|
62
|
+
timeout: float = DEFAULT_HTTP_TIMEOUT_S,
|
|
63
|
+
max_retries: int = MAX_RETRIES,
|
|
64
|
+
logger: Logger | None = None,
|
|
65
|
+
) -> httpx.Response:
|
|
66
|
+
last_error: Exception | None = None
|
|
67
|
+
backoff_s = INITIAL_BACKOFF_S
|
|
68
|
+
path = httpx.URL(url).path
|
|
69
|
+
owns_client = client is None
|
|
70
|
+
|
|
71
|
+
for attempt in range(max_retries + 1):
|
|
72
|
+
start = time.monotonic()
|
|
73
|
+
try:
|
|
74
|
+
http = client or httpx.AsyncClient(timeout=timeout)
|
|
75
|
+
try:
|
|
76
|
+
res = await http.request(
|
|
77
|
+
method,
|
|
78
|
+
url,
|
|
79
|
+
headers=headers,
|
|
80
|
+
json=json_body,
|
|
81
|
+
timeout=timeout,
|
|
82
|
+
)
|
|
83
|
+
finally:
|
|
84
|
+
if owns_client:
|
|
85
|
+
await http.aclose()
|
|
86
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
87
|
+
if res.is_success or (res.status_code < 500 and res.status_code != 429):
|
|
88
|
+
if logger:
|
|
89
|
+
logger.debug(
|
|
90
|
+
"http response",
|
|
91
|
+
{
|
|
92
|
+
"method": method,
|
|
93
|
+
"path": path,
|
|
94
|
+
"status": res.status_code,
|
|
95
|
+
"elapsed_ms": elapsed_ms,
|
|
96
|
+
"attempt": attempt + 1,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
return res
|
|
100
|
+
last_error = RuntimeError(f"HTTP {res.status_code}")
|
|
101
|
+
except Exception as err:
|
|
102
|
+
last_error = err
|
|
103
|
+
|
|
104
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
105
|
+
if attempt < max_retries:
|
|
106
|
+
if logger:
|
|
107
|
+
logger.debug(
|
|
108
|
+
"http retry",
|
|
109
|
+
{
|
|
110
|
+
"method": method,
|
|
111
|
+
"path": path,
|
|
112
|
+
"attempt": attempt + 1,
|
|
113
|
+
"max_retries": max_retries,
|
|
114
|
+
"elapsed_ms": elapsed_ms,
|
|
115
|
+
"backoff_ms": int(backoff_s * 1000),
|
|
116
|
+
"error": str(last_error),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
await asyncio.sleep(backoff_s)
|
|
120
|
+
backoff_s = min(backoff_s * 2, MAX_BACKOFF_S)
|
|
121
|
+
else:
|
|
122
|
+
if logger:
|
|
123
|
+
logger.warn(
|
|
124
|
+
"http exhausted retries",
|
|
125
|
+
{
|
|
126
|
+
"method": method,
|
|
127
|
+
"path": path,
|
|
128
|
+
"attempts": attempt + 1,
|
|
129
|
+
"elapsed_ms": elapsed_ms,
|
|
130
|
+
"error": str(last_error),
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
raise last_error # type: ignore[misc]
|