superred 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.
- superred-0.1.0/.github/workflows/ci.yml +47 -0
- superred-0.1.0/.github/workflows/release.yml +113 -0
- superred-0.1.0/.gitignore +20 -0
- superred-0.1.0/LICENSE +21 -0
- superred-0.1.0/MUTATIONS.md +166 -0
- superred-0.1.0/PKG-INFO +175 -0
- superred-0.1.0/README.md +140 -0
- superred-0.1.0/RELEASING.md +136 -0
- superred-0.1.0/TESTING.md +213 -0
- superred-0.1.0/docs/CNAME +1 -0
- superred-0.1.0/docs/_config.yml +8 -0
- superred-0.1.0/docs/_data/modules.yml +35 -0
- superred-0.1.0/docs/_data/nav.yml +40 -0
- superred-0.1.0/docs/_layouts/doc.html +50 -0
- superred-0.1.0/docs/_layouts/page.html +36 -0
- superred-0.1.0/docs/assets/home.css +243 -0
- superred-0.1.0/docs/assets/site.css +75 -0
- superred-0.1.0/docs/assets/style.css +178 -0
- superred-0.1.0/docs/guide/README.md +55 -0
- superred-0.1.0/docs/guide/advanced-patterns.md +203 -0
- superred-0.1.0/docs/guide/core-concepts.md +239 -0
- superred-0.1.0/docs/guide/quick-start.md +136 -0
- superred-0.1.0/docs/guide/running-evaluations.md +335 -0
- superred-0.1.0/docs/guide/security-domains.md +366 -0
- superred-0.1.0/docs/guide/writing-a-target.md +371 -0
- superred-0.1.0/docs/guide/writing-an-optimizer.md +386 -0
- superred-0.1.0/docs/guide/writing-tasks.md +241 -0
- superred-0.1.0/docs/index.html +166 -0
- superred-0.1.0/docs/modules.html +39 -0
- superred-0.1.0/docs/portfolio.html +28 -0
- superred-0.1.0/docs/reference/architecture.md +200 -0
- superred-0.1.0/docs/reference/breaking-changes.md +317 -0
- superred-0.1.0/docs/reference/controller.md +264 -0
- superred-0.1.0/docs/reference/optimizer.md +152 -0
- superred-0.1.0/docs/reference/security-claim.md +44 -0
- superred-0.1.0/docs/reference/target.md +89 -0
- superred-0.1.0/docs/reference/task.md +63 -0
- superred-0.1.0/docs/reference/types.md +304 -0
- superred-0.1.0/pyproject.toml +120 -0
- superred-0.1.0/src/superred/__init__.py +3 -0
- superred-0.1.0/src/superred/core/__init__.py +111 -0
- superred-0.1.0/src/superred/core/channel.py +227 -0
- superred-0.1.0/src/superred/core/controller.py +1056 -0
- superred-0.1.0/src/superred/core/interfaces/__init__.py +14 -0
- superred-0.1.0/src/superred/core/interfaces/optimizer.py +240 -0
- superred-0.1.0/src/superred/core/interfaces/security_claim.py +76 -0
- superred-0.1.0/src/superred/core/interfaces/target.py +180 -0
- superred-0.1.0/src/superred/core/interfaces/task.py +113 -0
- superred-0.1.0/src/superred/core/llm.py +166 -0
- superred-0.1.0/src/superred/core/middleware.py +114 -0
- superred-0.1.0/src/superred/core/persistence.py +501 -0
- superred-0.1.0/src/superred/core/types/__init__.py +71 -0
- superred-0.1.0/src/superred/core/types/controllable.py +31 -0
- superred-0.1.0/src/superred/core/types/evaluation.py +68 -0
- superred-0.1.0/src/superred/core/types/event.py +63 -0
- superred-0.1.0/src/superred/core/types/events.py +187 -0
- superred-0.1.0/src/superred/core/types/goal.py +16 -0
- superred-0.1.0/src/superred/core/types/llm.py +64 -0
- superred-0.1.0/src/superred/core/types/observable.py +44 -0
- superred-0.1.0/src/superred/core/types/security_domain.py +156 -0
- superred-0.1.0/src/superred/core/types/state.py +73 -0
- superred-0.1.0/src/superred/core/types/trajectory.py +167 -0
- superred-0.1.0/src/superred/py.typed +0 -0
- superred-0.1.0/tests/__init__.py +0 -0
- superred-0.1.0/tests/conftest.py +341 -0
- superred-0.1.0/tests/test_channel.py +337 -0
- superred-0.1.0/tests/test_controller.py +2080 -0
- superred-0.1.0/tests/test_controller_persistence.py +628 -0
- superred-0.1.0/tests/test_distinct_combinations.py +110 -0
- superred-0.1.0/tests/test_dynamic_scope.py +1006 -0
- superred-0.1.0/tests/test_evaluation_result_validation.py +46 -0
- superred-0.1.0/tests/test_integration.py +998 -0
- superred-0.1.0/tests/test_llm.py +696 -0
- superred-0.1.0/tests/test_middleware.py +257 -0
- superred-0.1.0/tests/test_mutations.py +1027 -0
- superred-0.1.0/tests/test_optimizer.py +267 -0
- superred-0.1.0/tests/test_persistence.py +669 -0
- superred-0.1.0/tests/test_properties.py +204 -0
- superred-0.1.0/tests/test_security_claim.py +102 -0
- superred-0.1.0/tests/test_security_domain.py +224 -0
- superred-0.1.0/tests/test_trajectory.py +398 -0
- superred-0.1.0/tests/test_types.py +399 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
cache: pip
|
|
32
|
+
|
|
33
|
+
- name: Install package with dev extras
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade pip
|
|
36
|
+
pip install -e ".[dev]"
|
|
37
|
+
|
|
38
|
+
- name: Lint (ruff)
|
|
39
|
+
run: |
|
|
40
|
+
ruff check src/ tests/
|
|
41
|
+
ruff format --check src/ tests/
|
|
42
|
+
|
|
43
|
+
- name: Type-check (mypy, strict)
|
|
44
|
+
run: mypy src/
|
|
45
|
+
|
|
46
|
+
- name: Test (pytest with coverage gate)
|
|
47
|
+
run: pytest
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes the package to PyPI when you publish a GitHub Release.
|
|
4
|
+
# Manually running this workflow (Actions tab -> Release -> Run workflow) does a
|
|
5
|
+
# dry run to TestPyPI instead, so you can rehearse the whole pipeline safely.
|
|
6
|
+
#
|
|
7
|
+
# Authentication uses PyPI Trusted Publishing (OIDC): there are no API tokens or
|
|
8
|
+
# secrets to store. See RELEASING.md for the one-time PyPI/GitHub setup.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
release:
|
|
12
|
+
types: [published]
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
testpypi:
|
|
16
|
+
description: "Dry run: build and publish to TestPyPI instead of PyPI"
|
|
17
|
+
type: boolean
|
|
18
|
+
default: true
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
build:
|
|
25
|
+
name: Build distributions
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- name: Set up Python
|
|
31
|
+
uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.13"
|
|
34
|
+
|
|
35
|
+
- name: Install build tooling
|
|
36
|
+
run: python -m pip install --upgrade build twine
|
|
37
|
+
|
|
38
|
+
- name: Check that __version__ matches the release tag
|
|
39
|
+
if: github.event_name == 'release'
|
|
40
|
+
env:
|
|
41
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
42
|
+
run: |
|
|
43
|
+
python - <<'PY'
|
|
44
|
+
import os, re, pathlib, sys
|
|
45
|
+
|
|
46
|
+
text = pathlib.Path("src/superred/__init__.py").read_text()
|
|
47
|
+
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', text)
|
|
48
|
+
version = match.group(1) if match else None
|
|
49
|
+
tag = os.environ["RELEASE_TAG"].lstrip("v")
|
|
50
|
+
print(f"Package version (src/superred/__init__.py): {version}")
|
|
51
|
+
print(f"Release tag: {tag}")
|
|
52
|
+
if version != tag:
|
|
53
|
+
print(
|
|
54
|
+
f"::error::__version__ ({version}) does not match the release "
|
|
55
|
+
f"tag ({tag}). Bump src/superred/__init__.py, then re-create the "
|
|
56
|
+
f"release/tag."
|
|
57
|
+
)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
PY
|
|
60
|
+
|
|
61
|
+
- name: Build sdist and wheel
|
|
62
|
+
run: python -m build
|
|
63
|
+
|
|
64
|
+
- name: Verify metadata renders on PyPI
|
|
65
|
+
run: python -m twine check dist/*
|
|
66
|
+
|
|
67
|
+
- name: Upload distributions
|
|
68
|
+
uses: actions/upload-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
publish-pypi:
|
|
74
|
+
name: Publish to PyPI
|
|
75
|
+
needs: build
|
|
76
|
+
if: github.event_name == 'release'
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
environment:
|
|
79
|
+
name: pypi
|
|
80
|
+
url: https://pypi.org/project/superred/
|
|
81
|
+
permissions:
|
|
82
|
+
id-token: write # required for Trusted Publishing (OIDC)
|
|
83
|
+
steps:
|
|
84
|
+
- name: Download distributions
|
|
85
|
+
uses: actions/download-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: dist
|
|
88
|
+
path: dist/
|
|
89
|
+
|
|
90
|
+
- name: Publish to PyPI
|
|
91
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
92
|
+
|
|
93
|
+
publish-testpypi:
|
|
94
|
+
name: Publish to TestPyPI (dry run)
|
|
95
|
+
needs: build
|
|
96
|
+
if: github.event_name == 'workflow_dispatch' && inputs.testpypi
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
environment:
|
|
99
|
+
name: testpypi
|
|
100
|
+
url: https://test.pypi.org/project/superred/
|
|
101
|
+
permissions:
|
|
102
|
+
id-token: write # required for Trusted Publishing (OIDC)
|
|
103
|
+
steps:
|
|
104
|
+
- name: Download distributions
|
|
105
|
+
uses: actions/download-artifact@v4
|
|
106
|
+
with:
|
|
107
|
+
name: dist
|
|
108
|
+
path: dist/
|
|
109
|
+
|
|
110
|
+
- name: Publish to TestPyPI
|
|
111
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
112
|
+
with:
|
|
113
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
.env
|
|
9
|
+
*.egg
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.coverage
|
|
13
|
+
coverage.json
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.hypothesis/
|
|
16
|
+
.mutmut-cache
|
|
17
|
+
|
|
18
|
+
# Local AI-agent steering pointers (machine-local, not versioned)
|
|
19
|
+
CLAUDE.md
|
|
20
|
+
AGENTS.md
|
superred-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 superred contributors
|
|
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,166 @@
|
|
|
1
|
+
# Mutation Testing
|
|
2
|
+
|
|
3
|
+
## Tool: mutmut v2.5
|
|
4
|
+
|
|
5
|
+
Configuration is in `pyproject.toml` under `[tool.mutmut]`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run all mutants (~2 minutes)
|
|
9
|
+
mutmut run --no-progress
|
|
10
|
+
|
|
11
|
+
# View results (may crash on Python 3.13 due to pony ORM bug —
|
|
12
|
+
# use sqlite query below as workaround)
|
|
13
|
+
mutmut results
|
|
14
|
+
|
|
15
|
+
# Inspect a specific mutant's diff
|
|
16
|
+
mutmut show <id>
|
|
17
|
+
|
|
18
|
+
# Query results directly from the sqlite cache
|
|
19
|
+
python -c "
|
|
20
|
+
import sqlite3
|
|
21
|
+
conn = sqlite3.connect('.mutmut-cache')
|
|
22
|
+
c = conn.cursor()
|
|
23
|
+
c.execute('SELECT status, count(*) FROM Mutant GROUP BY status')
|
|
24
|
+
for status, count in c.fetchall(): print(f' {status}: {count}')
|
|
25
|
+
"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Results (2026-04-02)
|
|
29
|
+
|
|
30
|
+
| Metric | Value |
|
|
31
|
+
|--------|-------|
|
|
32
|
+
| Total mutants | 324 |
|
|
33
|
+
| Killed (by mutmut) | 196 |
|
|
34
|
+
| Timeout | 6 |
|
|
35
|
+
| Survived (by mutmut) | 122 |
|
|
36
|
+
| Import-crash (killed but miscounted by mutmut) | 9 |
|
|
37
|
+
| Equivalent/unkillable | 83 |
|
|
38
|
+
| **Effectively killed** | **211** |
|
|
39
|
+
| **Raw score** (mutmut reported) | **62.3%** |
|
|
40
|
+
| **Adjusted score** (excl. equivalents, incl. import-crash) | **87.6%** (211/241) |
|
|
41
|
+
|
|
42
|
+
### Note on import-crash mutants
|
|
43
|
+
|
|
44
|
+
9 mutants in `security_domain.py` crash at conftest import time (pytest exit code 4 =
|
|
45
|
+
"collection error"). mutmut counts these as "survived" because no test assertion fired,
|
|
46
|
+
but the mutation does cause an immediate crash — they are effectively killed. These are
|
|
47
|
+
validation mutations (duplicate check, orphan check) that break `SecurityDomain()`
|
|
48
|
+
construction used in `conftest.py`.
|
|
49
|
+
|
|
50
|
+
### Per-module breakdown
|
|
51
|
+
|
|
52
|
+
| Module | Kill | Surv | TO | Total | Score |
|
|
53
|
+
|--------|------|------|----|-------|-------|
|
|
54
|
+
| types/goal.py | 2 | 0 | 0 | 2 | 100% |
|
|
55
|
+
| types/evaluation.py | 10 | 0 | 0 | 10 | 100% |
|
|
56
|
+
| types/controllable.py | 10 | 0 | 0 | 10 | 100% |
|
|
57
|
+
| types/observable.py | 8 | 1 | 0 | 9 | 89% |
|
|
58
|
+
| middleware.py | 8 | 2 | 0 | 10 | 80% |
|
|
59
|
+
| interfaces/optimizer.py | 14 | 4 | 1 | 19 | 79% |
|
|
60
|
+
| types/state.py | 5 | 2 | 0 | 7 | 71% |
|
|
61
|
+
| channel.py | 20 | 11 | 4 | 35 | 69% |
|
|
62
|
+
| interfaces/security_claim.py | 11 | 6 | 0 | 17 | 65% |
|
|
63
|
+
| types/trajectory.py | 19 | 11 | 0 | 30 | 63% |
|
|
64
|
+
| types/security_domain.py | 24 | 15 | 0 | 39 | 62% |
|
|
65
|
+
| controller.py | 50 | 33 | 1 | 84 | 61% |
|
|
66
|
+
| types/event.py | 11 | 21 | 0 | 32 | 34% |
|
|
67
|
+
| interfaces/target.py | 0 | 14 | 0 | 14 | 0% |
|
|
68
|
+
| interfaces/task.py | 0 | 6 | 0 | 6 | 0% |
|
|
69
|
+
|
|
70
|
+
Note: raw per-module scores are low because they include equivalent mutants
|
|
71
|
+
(decorators, string literals, abstract stubs). The adjusted score excludes these.
|
|
72
|
+
|
|
73
|
+
## Equivalent Mutants (83 total)
|
|
74
|
+
|
|
75
|
+
These are mutations that cannot be detected because they don't change observable behavior.
|
|
76
|
+
|
|
77
|
+
### Decorator mutations (~23)
|
|
78
|
+
`@dataclass(frozen=True)` → `@dataclass(frozen=False)` and `kw_only=True` → `kw_only=False`.
|
|
79
|
+
These change Python's dataclass machinery, not application logic.
|
|
80
|
+
|
|
81
|
+
### Abstract interface stubs (~20)
|
|
82
|
+
`interfaces/target.py` and `interfaces/task.py` contain only abstract method signatures.
|
|
83
|
+
The ABCs are never instantiated directly — all mutations in these files are unkillable.
|
|
84
|
+
|
|
85
|
+
### String literal mutations (~25)
|
|
86
|
+
Error messages, log format strings, print output, and description strings on entry types.
|
|
87
|
+
Changing these strings has no behavioral effect.
|
|
88
|
+
|
|
89
|
+
### Type alias / TypeVar definitions (~5)
|
|
90
|
+
`EventHandler = Callable[...]`, `Middleware = Callable[...]`, `T_Target = TypeVar(...)`.
|
|
91
|
+
Type-level constructs with no runtime behavior.
|
|
92
|
+
|
|
93
|
+
### Initialization sentinel values (~7)
|
|
94
|
+
`self._closed = False`, `self._drain_cursor = 0`, `first_error = None`, etc.
|
|
95
|
+
mutmut changes these to different falsy/truthy values, not to meaningful alternatives.
|
|
96
|
+
|
|
97
|
+
### EventChannel loop capture (~2)
|
|
98
|
+
`channel.py:105-106`: The `_loop` capture in `send()` enables cross-thread `close()`.
|
|
99
|
+
When mutated, `close()` falls back to `put_nowait(None)` which works identically in
|
|
100
|
+
single-threaded async tests. Only detectable in a cross-thread race condition.
|
|
101
|
+
|
|
102
|
+
### SecurityClaim._claims field (~1)
|
|
103
|
+
`security_claim.py:54`: `_claims = None` → `_claims = ""`. For `from_tasks` claims,
|
|
104
|
+
`_claims` is never checked — `__iter__` takes the `_tasks is not None` branch first.
|
|
105
|
+
|
|
106
|
+
## Remaining Survivors (~30)
|
|
107
|
+
|
|
108
|
+
After killing the high-value mutants and reclassifying equivalents, ~30 mutants
|
|
109
|
+
remain. These are all in one of these categories:
|
|
110
|
+
|
|
111
|
+
- **Cosmetic print/log output** in `controller.py._print_summary` — string formatting,
|
|
112
|
+
`print()` calls, status labels. No behavioral contract.
|
|
113
|
+
- **Dataclass field defaults** — `field(default_factory=list)`, `max_runs_per_task: int = 100`.
|
|
114
|
+
Changing the default value is only observable if no value is passed at construction.
|
|
115
|
+
- **`@dataclass(frozen=True)` on event types** — 21 mutations across `event.py`.
|
|
116
|
+
Each event type's `frozen=True` could be tested with `FrozenInstanceError` assertions,
|
|
117
|
+
but we already test this pattern on representative types in `test_types.py`.
|
|
118
|
+
|
|
119
|
+
None of these represent behavioral risk.
|
|
120
|
+
|
|
121
|
+
## Manual Mutation Tests (tests/test_mutations.py)
|
|
122
|
+
|
|
123
|
+
40 hand-written tests targeting specific high-value mutations. Each test names
|
|
124
|
+
the exact mutation it kills in its docstring. These complement mutmut by
|
|
125
|
+
covering patterns that require specific assertion strategies (tie-breaking,
|
|
126
|
+
latching, defensive copies, exception-safe teardown).
|
|
127
|
+
|
|
128
|
+
## Per-task scope resolver (2026-06-18)
|
|
129
|
+
|
|
130
|
+
The per-task scope feature (`ScopeResolver`, `_TaskScope`, per-task resolution
|
|
131
|
+
of BOTH `scope` and `read_only` in `run_one` / `_task_scope_for` with the
|
|
132
|
+
empty-visibility skip rule, and the `scope_label` naming path) touches 246 lines
|
|
133
|
+
in `controller.py` and 64 in `persistence.py`. A full-core mutmut run (783
|
|
134
|
+
mutants) leaves the touched lines with:
|
|
135
|
+
|
|
136
|
+
| File | Behavioral mutants killed | Survivors (all equivalent) | Adjusted |
|
|
137
|
+
|------|---------------------------|----------------------------|----------|
|
|
138
|
+
| persistence.py | 13 | 0 | 100% |
|
|
139
|
+
| controller.py | 48 | 39 | 100% |
|
|
140
|
+
|
|
141
|
+
No surviving mutant changes a branch condition, comparison, boolean, arithmetic
|
|
142
|
+
operation, or return value. In particular the new behavioral logic is fully
|
|
143
|
+
killed: the `if not visibility` skip branch, the two `except NotApplicable`
|
|
144
|
+
handlers, and the `callable(scope) or callable(read_only)` discriminator all
|
|
145
|
+
have their mutants caught by the skip/run/error tests, consistent with the 100%
|
|
146
|
+
line-and-branch coverage of the diff. The 39 `controller.py` survivors (over 27
|
|
147
|
+
distinct lines) are all equivalent mutants of classes already documented above:
|
|
148
|
+
|
|
149
|
+
- **Type alias, annotation, and `cast()`**: `ScopeResolver = None`; the
|
|
150
|
+
`str & None` annotation on `error` / `scope_label` / `detail_basename` /
|
|
151
|
+
`static_scope` / `static_read_only`; and `cast("XX...XX", ...)`. Instance- and
|
|
152
|
+
local-variable annotations are not evaluated at runtime (the `str & None`
|
|
153
|
+
mutants survive instead of crashing import, which proves it), and `cast()`'s
|
|
154
|
+
first argument is a runtime no-op.
|
|
155
|
+
- **Validation, skip, log, and rationale strings**: the three validation
|
|
156
|
+
`ValueError` messages, the `_task_scope_for` empty-visibility `NotApplicable`
|
|
157
|
+
message, the `logger` format strings, and the synthesized-error `rationale=`
|
|
158
|
+
strings. The validation tests assert the correct error fired
|
|
159
|
+
(`pytest.raises(..., match=<key phrase>)`); mutmut only wraps the message as
|
|
160
|
+
`XX...XX`, which the interior substring match still satisfies, so the residual
|
|
161
|
+
mutant is purely cosmetic.
|
|
162
|
+
- **Cosmetic `_print_summary` output**: `scope_label` / scope-name formatting
|
|
163
|
+
and `"=" * 60` decoration in the dynamic-mode summary branch.
|
|
164
|
+
- **Dataclass / local defaults**: `error` and `detail_basename` `None -> ""`,
|
|
165
|
+
observable only if no value is passed; `""` is falsy like `None` in the only
|
|
166
|
+
context `detail_basename` is read.
|
superred-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: superred
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modular framework for comprehensive red-teaming of AI systems
|
|
5
|
+
Project-URL: Homepage, https://superred.simonsure.com
|
|
6
|
+
Project-URL: Documentation, https://superred.simonsure.com
|
|
7
|
+
Project-URL: Repository, https://github.com/RoldSI/superred
|
|
8
|
+
Project-URL: Issues, https://github.com/RoldSI/superred/issues
|
|
9
|
+
Author: SuperRed Team
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: adversarial,ai-safety,jailbreak,llm,red-teaming,security
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: <3.14,>=3.11
|
|
22
|
+
Requires-Dist: litellm>=1.89.0
|
|
23
|
+
Provides-Extra: bedrock
|
|
24
|
+
Requires-Dist: boto3>=1.40; extra == 'bedrock'
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
27
|
+
Requires-Dist: mutmut<3,>=2.5; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# superred
|
|
37
|
+
|
|
38
|
+
A modular framework for red-teaming AI systems. You point an automated attacker (an **optimizer**) at an AI system (a **target**) and measure whether it can make the system misbehave according to the adversarial goal of a **security claim**, under a precisely defined level of access (a **security domain scope**).
|
|
39
|
+
|
|
40
|
+
> APIs may change;
|
|
41
|
+
> see [`docs/reference/breaking-changes.md`](docs/reference/breaking-changes.md).
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Overview
|
|
45
|
+
|
|
46
|
+
Target modules, optimizer modules, and security claims should generally be independent and freely combine; any optimizer can be used to attack any target. Some combinations may not make sense but should still technically run, such as running an optimizer, whose strategy attempts to elicit unsafe model behavior via only a user prompt, against a complex agent. Further, security claims may be specific to targets for precise claims, but the underlying target must still function without the security claim
|
|
47
|
+
|
|
48
|
+
### TARGET
|
|
49
|
+
A target is a module that wraps or ports some real system target (except the provided simple ChatbotTarget). Such targets should remain functionally unchanged from the original work. Targets are general purpose. No matter their origin (benchmark, real system, ...) they should exist independently with their full capabilities connected to the framework
|
|
50
|
+
|
|
51
|
+
They must expose EVERY part of the system that may somehow be relevant to an attacker as an `Observable` or `Controllable` (and `ConfigSpec` & `QuerySpec`)
|
|
52
|
+
|
|
53
|
+
`Observable`s give insight into the state and functioning of the target system. `Controllables` allow meddling with the target system. I.e., `Observable`s are for passive interactions with the target and `Controllable`s are for active interaction with the target. `Controllable`s always allow full editing of the data at the controlled interface.
|
|
54
|
+
Both are means for an optimizer to perform an attack. `Observable`s yield information to an optimizer, `Controllable`s are how the attacker specifically facilitates its attack. Specifically, `Controllables` are everything *relevant* that an attacker who compromised everything can meddle with
|
|
55
|
+
|
|
56
|
+
- Relevant to an attacker in terms of `Controllables` is everything that may end up in context of an LLM, influence the context of an LLM, or influence the trajectory of the system. This may be the system prompt, the user input, the content a tool returns, a retrieved document, or memory carried across runs
|
|
57
|
+
- Relevant to an attacker in terms of `Observables` is everything that provides insights into the functioning of the target system: What happens? When? Why? How? It should include tool calls, model inputs/outputs, ... Combining all observables must yield a completely exhaustive trajectory of the AI system
|
|
58
|
+
- `ConfigSpec` and `QuerySpec` must allow full access to ALL target system state that impacts the behavior of the system while running
|
|
59
|
+
|
|
60
|
+
Information should only be emitted once as `Observable` or `Controllable`. `ConfigSpec` and `QuerySpec` are independent
|
|
61
|
+
|
|
62
|
+
Static configuration is exposed as static `Observable`s, read once by the optimizer at initialization; everything that *happens* during a run is emitted onto the `Trajectory` as `ObservableEvent`s. Facts about the setup belong to `get_observables`; the live run trace belongs on the trajectory.
|
|
63
|
+
Note that `Controllable`s also implicitly provide information about the state during runtime. The `Controller` makes sure that this information is available on the `Trajectory`. The target must NOT emit tool call information separately as a `Controllable` and `Observable` but only once as a `Controllable`
|
|
64
|
+
|
|
65
|
+
`ConfigSpec` is set by the `Task` before a run, not by the attacker, so a config slot is never itself an attacker surface; `Controllable`s are the attacker-facing injection points. Model identity and generation settings stay out of `ConfigSpec`: they are construction concerns, fixed for an experiment
|
|
66
|
+
|
|
67
|
+
A target is created fresh per `Task`. Its lifecycle is construct, then `configure_target` once, then a loop of run / evaluate / `reset_ephemeral_state` (up to the run budget), then `teardown`. `reset_ephemeral_state` clears only per-run ephemeral state and must preserve per-task configuration and any durable memory carried across runs; `teardown` releases external resources
|
|
68
|
+
|
|
69
|
+
A target runs inference with its own provider client, not the optimizer's `LLMClient`, so its token spend is out of band and uncounted against the optimizer's budget
|
|
70
|
+
|
|
71
|
+
The `TargetFactory` exists so that an experiment may obtain several targets if it allows parallelism. Then the target factory can decide whether it wants to reuse instances. The tendency should be to discard and newly instantiate target instances (guarantees clean state) unless it is extremely expensive. Instantiating parallelism is the factory's concern. The target's concern is to make each instance run independently and without interference: target instances are independent and carry no concurrency cap of their own, so the controller can run as many in parallel as the factory's concurrency allows
|
|
72
|
+
|
|
73
|
+
#### Security Domain Forest
|
|
74
|
+
To an optimizer, a target is characterized in addition to its `Controllable`s and `Observable`s by the trust boundaries of the target AI system. Trust boundaries correspond to how components of the target AI system which can be separately compromised; such as the memory system, a database, tool A, tool B, etc.
|
|
75
|
+
|
|
76
|
+
Each trust boundary is represented by a `SecurityDomainTag`. `SecurityDomainTag`s should follow real trust boundaries, the loci that could realistically be compromised (an external service, a database, a memory system), rather than one tag per surface; surfaces sharing a boundary share a tag. The memory system has its own tag, tool A has its own tag, etc. Further, those tags have a hierarchical forest structure. So that tool A can have sub-scopes A.1 and A.2; e.g., a database with subscopes orders and customers. Having access to a scope means having access to all its subscopes
|
|
77
|
+
|
|
78
|
+
Commonly, we have `system` and `user` as root tags, where `system` may have as subscopes `system_prompt`, `model_responses` (which allows edit access to model inference results), `architecture` (with sub-sub scope `high_level_architecture`), ...
|
|
79
|
+
|
|
80
|
+
We do not distinguish read and write tags. The `Controller` may only give read access for some `SecurityDomainTag` and handles making the runtime data available on the trajectory. This is invisible to the `Target` which emits `Controllable` `Event`s and receives responses unchanged
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
### OPTIMIZER
|
|
84
|
+
An optimizer is a module that implements some adversarial strategy. It may be a new strategy, a port of an existing strategy, a wrapper for an existing strategy's implementation, etc.
|
|
85
|
+
|
|
86
|
+
The optimizer is the only actively adversarial component in the framework. The `Target` is a passive attack surface and the `SecurityClaim` defines the `Goal` and judges the outcome; the optimizer's job is to drive the target into violating a security property by achieving the specified goal using the access it is granted. It acts ONLY through `Controllable`s and observes ONLY through `Observable`s and the `Trajectory`, and never touches the target directly
|
|
87
|
+
|
|
88
|
+
The interaction is event-driven. At runtime initialization the optimizer receives the `Goal`, the in-scope `Controllable`s and `Observable`s, and an `LLMClient`. During a run it consumes a stream of `Event`s (the same events the `Controller` routes, described below) and answers each one: `ControllableInjection` to act, supplying the value to inject; `ControllableNoInjection` to decline; or `RunEndResponse` to end or continue a run. `on_event` is the per-event handler, and the default sequential loop may be overridden for parallel or continuous consumption. An optimizer that always declines is the passthrough baseline: it changes nothing and reproduces the target's unattacked behavior, so every real attack is a choice of which `Controllable` to inject and with what value
|
|
89
|
+
|
|
90
|
+
A `Task` may run several times, up to the per-task budget. The `RunEndEvent` carries the evaluation feedback when the experiment enables it; the optimizer uses that feedback to steer its `RunEndResponse(done=...)` and its next attempt. Success is decided by the `SecurityClaim`, not the optimizer: feedback is for steering the next attempt, not for self-certifying a win
|
|
91
|
+
|
|
92
|
+
If based on an existing strategy, the implementation must be faithful to the original work. This means it should adhere to the design intentions of the original work and implement that strategy to attack. Whenever possible, the code and data should be byte-identical to the upstream. This must be guaranteed and verified by actually fetching the upstream source code into a `tmp` folder.
|
|
93
|
+
Some modifications may have to be made to adapt a strategy to the `superred` framework. Those modifications should stay faithful to the intention of the original work while making sure the implementation adequately uses framework capabilities. For example:
|
|
94
|
+
- Optimizers may be designed for a fixed number of iterations. The `superred` controller, however, enforces budget limits. So the module implementation/port should keep going until it hits a budget exhaust error
|
|
95
|
+
- A strategy may be specialized in memory attacks. To do direct memory injection, it may need new functionality to identify which controllables are memory-related. For such tasks it could use an LLM
|
|
96
|
+
- A strategy may assume a single specified injection point. But the optimizer has to choose its own injection point and a small extension could vary it between attempts, if smart?
|
|
97
|
+
- A strategy may be designed to vary a pre-existing injection. But the optimizer may also have to generate an initial text to bootstrap from
|
|
98
|
+
- etc.
|
|
99
|
+
|
|
100
|
+
Optimizers are general purpose and not specific to targets nor security claim. No matter their origin (benchmark, existing strategy, new strategy) the optimizer should be able to attack a general target with unknown structure, an unknown set of observables with unknown names, an unknown set of controllables with unknown names, etc.
|
|
101
|
+
The scope of available `Controllable`s may be different from an intended set. For instance, it may be that an optimizer reliant upon memory systems is run in a scope where the memory system is not exposed directly. It is a design decision how to handle this, but the optimizer should generally never crash in unexpected scenarios.
|
|
102
|
+
Not covering a certain scenario and having the optimizer give up should only be a last resort if this strategy is inherently incompatible with the given situation. Reasonable minimal extensions are justified if they don't change fundamental functionality and are in line with the original work's intention
|
|
103
|
+
|
|
104
|
+
`Optimizer`s may use all `Controllable`s available to them or choose to only utilize a subset. Within the scope of the implemented strategy, an `Optimizer` should use everything at its disposal in the strongest possible way. We will run different experiments with different access scopes of varying sets of `Controllable`s and `Observable`s. This is enforced by the controller and invisible to the `Optimizer` however, which only sees its current scope. Thus, it should always make strongest use of it
|
|
105
|
+
|
|
106
|
+
`Optimizer` instances are provided by an `OptimizerFactory`. It is required that arbitrarily many instances of an `Optimizer` implementation can be created in parallel without interference. Instantiation is standardized per experiment and the `Optimizer` has to adapt through runtime initialization to the given scenario. This means there should be no configuration (required) at `Optimizer` object creation; the `Optimizer` should infer all information itself from the data it gets during the runtime initialization call. Determining the required information and configuration from this runtime initialization may be done using an LLM call; also as an extension to the original work if sensible
|
|
107
|
+
|
|
108
|
+
Optimizers receive an `LLMClient`, which they may use to do model inference. No matter the original strategy, an optimizer has no control over the underlying model or the available total budget. Both are fixed for an experiment
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
### Controller
|
|
112
|
+
The controller orchestrates running an `Optimizer` against a `Target` to achieve some adversarial `Goal` (from a `SecurityClaim`), while enforcing a security scope.
|
|
113
|
+
|
|
114
|
+
To connect an `Optimizer` and `Target`, the `superred` framework uses `Event`s. The `Target` emits `Event`s. The `Controller` puts these on the `Trajectory` (if not actionable for the `Optimizer`) or forwards them to the `Optimizer` via the `EventChannel`.
|
|
115
|
+
If put on the `Trajectory`: The `Optimizer` has access to a `FilteredTrajectory` which is filtered down the security scope
|
|
116
|
+
If forwarded to the `Optimizer`, `Event`s outside of the security scope are filtered out first. The `Optimizer` receives an `EventEnvelope` with one of the `Event`s: `ObservableEvent`, `ControllablePreCallEvent`, `ControllablePostCallEvent`, `RunStartEvent`, `RunEndEvent`. It can respond with either `ControllableInjection` (it acts) or `ControllableNoInjection` (it does not act) or `RunEndResponse` (to `RunEndEvent` only)
|
|
117
|
+
|
|
118
|
+
So, the `Target` uses `Event`s to share information and expose `Controllable`s. The `Controller` filters `Event`s to fit the threat model of one experiment, puts them on the `Trajectory` for the attacker to read, or forwards them to the `Optimizer` for the attacker to act
|
|
119
|
+
|
|
120
|
+
A `Trajectory` holds only `Event`s and `EventResponse`s, and a `Target` writes to it in two ways. One-way `ObservableEvent`s carry an `Observable` and are recorded directly. Two-way `Controllable` events go through the `EventChannel`, where the controller records both the event and the optimizer's response. Every persisted item carries a security domain, and the optimizer's `FilteredTrajectory` view shows only what its scope includes
|
|
121
|
+
|
|
122
|
+
The `Controller` runs many `Optimizer`s against many `Target`s (in parallel). Part of running the `Controller` is specifying a `SecurityClaim`, which is a set of `Tasks` of adversarial `Goal`s. For each `Task`/`Goal`, one `Optimizer`-`Target` instance is run
|
|
123
|
+
|
|
124
|
+
#### security scope
|
|
125
|
+
The security scope is determined by (a) subset of the security domain tree, (b) `Optimizer` LLM, and (c) `Optimizer` LLM budget per task; all three are configured during controller initialization
|
|
126
|
+
|
|
127
|
+
(a) is enforced by the `Controller` through filtering the availability of emitted `Event`s to the `Optimizer`. (c) is enforced by the `Controller` through monitoring expenditure and blocking requests once the limit is reached
|
|
128
|
+
|
|
129
|
+
### SECURITY CLAIM
|
|
130
|
+
A `SecurityClaim` is a set of `Task`s. It may either directly specify `Task`s or combine some set of existing `SecurityClaim`s into a new set of `Task`s
|
|
131
|
+
|
|
132
|
+
A `Task` specifies an adversarial `Goal`. After `Target` instantiation but before the `Optimizer` gets to work, the `Task` can configure the `Target` using `ConfigSpec`. After the `Optimizer` finishes, it further performs evaluation of the adversarial goal using the `QuerySpec` exposed by the `Target`
|
|
133
|
+
|
|
134
|
+
A `SecurityClaim` should represent some (set of) properties that are claimed to hold on a `Target`, and which an `Optimizer` may disprove. As such they may represent security properties such as confidentiality, integrity, etc.
|
|
135
|
+
|
|
136
|
+
`SecurityClaim`s can be either general or specific to a certain `Target`.
|
|
137
|
+
If general, the claim should not make any assumption on the `Target`, its functioning or its form. It may make general claims and needs to have some agentic component to adapt to different `Targets`, configure/evaluate them adequately, and provide sensible `Goal`s.
|
|
138
|
+
If specific, the claim can utilize knowledge of the `Target` configuration and provide `Goal`s that are adapted to the `Target`'s purpose
|
|
139
|
+
|
|
140
|
+
When adapted from a benchmark, a `SecurityClaim` should be faithful to the original and be byte-identical in its adversarial prompts and evaluation. If evaluation is agentic, it may only be acceptable to substitute the LLM to one available on the experiment proxy.
|
|
141
|
+
It may be that the `SecurityClaim` comes from a benchmark that provides both a target and adversarial goals. In such cases it may be sensible to make the security claim specific to the corresponding `Target`
|
|
142
|
+
|
|
143
|
+
A `Task` is stateless: it holds only immutable per-objective configuration and reads its result from the target's post-run state. Its score carries an always-delivered primary signal, which is unscoped, alongside any number of scoped sub-scores. A benchmark `SecurityClaim` is usually a small hierarchy: the full set of behaviors, subclaims such as one per harm category, and a convenience factory that builds a matching target. Any judges or scorers the claim runs to evaluate do not count against the optimizer's budget, like the target's own inference
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
## Packaging
|
|
147
|
+
`superred` is the framework package. The things you plug into it are shipped as separate, independently installable packages:
|
|
148
|
+
|
|
149
|
+
- **`superred`** (this repo) - the framework: interfaces, the controller, the event/trajectory/security-domain types
|
|
150
|
+
- **`superred-modules`** - the optimizers, targets, and security claims you run
|
|
151
|
+
- **`superred-experiments`** - scripts that wire specific combinations together
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Install
|
|
155
|
+
|
|
156
|
+
Python 3.11 to 3.13 (3.14 is not supported).
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# run from the workspace root that holds both the superred and superred-modules folders
|
|
160
|
+
python -m venv .venv && source .venv/bin/activate
|
|
161
|
+
pip install -e "./superred[dev]" # the framework, with test/lint extras
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Then install whichever modules you need from `superred-modules/` (each is its
|
|
165
|
+
own `pip install -e ./superred-modules/<kind>/<name>`)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## Documentation
|
|
169
|
+
|
|
170
|
+
The documentation is published as a website at **[superred.simonsure.com](https://superred.simonsure.com/)**. The same pages live as Markdown under [`docs/`](docs/):
|
|
171
|
+
|
|
172
|
+
- **[User Guide](docs/guide/README.md)** - start here if you want to *use* the framework: write targets, optimizers, tasks, and run evaluations
|
|
173
|
+
- [Quick Start](docs/guide/quick-start.md) runs an evaluation end to end
|
|
174
|
+
- **[Design Docs](docs/reference/architecture.md)** - the internal design and rationale: The [Controller](docs/reference/controller.md), [Optimizer](docs/reference/optimizer.md), [Target](docs/reference/target.md), [Task](docs/reference/task.md), [SecurityClaim](docs/reference/security-claim.md), and [Types](docs/reference/types.md)
|
|
175
|
+
- **[TESTING.md](TESTING.md)** - the test suite, coverage targets, and mutation testing. [MUTATIONS.md](MUTATIONS.md) contains notes on surviving mutants.
|