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.
Files changed (82) hide show
  1. superred-0.1.0/.github/workflows/ci.yml +47 -0
  2. superred-0.1.0/.github/workflows/release.yml +113 -0
  3. superred-0.1.0/.gitignore +20 -0
  4. superred-0.1.0/LICENSE +21 -0
  5. superred-0.1.0/MUTATIONS.md +166 -0
  6. superred-0.1.0/PKG-INFO +175 -0
  7. superred-0.1.0/README.md +140 -0
  8. superred-0.1.0/RELEASING.md +136 -0
  9. superred-0.1.0/TESTING.md +213 -0
  10. superred-0.1.0/docs/CNAME +1 -0
  11. superred-0.1.0/docs/_config.yml +8 -0
  12. superred-0.1.0/docs/_data/modules.yml +35 -0
  13. superred-0.1.0/docs/_data/nav.yml +40 -0
  14. superred-0.1.0/docs/_layouts/doc.html +50 -0
  15. superred-0.1.0/docs/_layouts/page.html +36 -0
  16. superred-0.1.0/docs/assets/home.css +243 -0
  17. superred-0.1.0/docs/assets/site.css +75 -0
  18. superred-0.1.0/docs/assets/style.css +178 -0
  19. superred-0.1.0/docs/guide/README.md +55 -0
  20. superred-0.1.0/docs/guide/advanced-patterns.md +203 -0
  21. superred-0.1.0/docs/guide/core-concepts.md +239 -0
  22. superred-0.1.0/docs/guide/quick-start.md +136 -0
  23. superred-0.1.0/docs/guide/running-evaluations.md +335 -0
  24. superred-0.1.0/docs/guide/security-domains.md +366 -0
  25. superred-0.1.0/docs/guide/writing-a-target.md +371 -0
  26. superred-0.1.0/docs/guide/writing-an-optimizer.md +386 -0
  27. superred-0.1.0/docs/guide/writing-tasks.md +241 -0
  28. superred-0.1.0/docs/index.html +166 -0
  29. superred-0.1.0/docs/modules.html +39 -0
  30. superred-0.1.0/docs/portfolio.html +28 -0
  31. superred-0.1.0/docs/reference/architecture.md +200 -0
  32. superred-0.1.0/docs/reference/breaking-changes.md +317 -0
  33. superred-0.1.0/docs/reference/controller.md +264 -0
  34. superred-0.1.0/docs/reference/optimizer.md +152 -0
  35. superred-0.1.0/docs/reference/security-claim.md +44 -0
  36. superred-0.1.0/docs/reference/target.md +89 -0
  37. superred-0.1.0/docs/reference/task.md +63 -0
  38. superred-0.1.0/docs/reference/types.md +304 -0
  39. superred-0.1.0/pyproject.toml +120 -0
  40. superred-0.1.0/src/superred/__init__.py +3 -0
  41. superred-0.1.0/src/superred/core/__init__.py +111 -0
  42. superred-0.1.0/src/superred/core/channel.py +227 -0
  43. superred-0.1.0/src/superred/core/controller.py +1056 -0
  44. superred-0.1.0/src/superred/core/interfaces/__init__.py +14 -0
  45. superred-0.1.0/src/superred/core/interfaces/optimizer.py +240 -0
  46. superred-0.1.0/src/superred/core/interfaces/security_claim.py +76 -0
  47. superred-0.1.0/src/superred/core/interfaces/target.py +180 -0
  48. superred-0.1.0/src/superred/core/interfaces/task.py +113 -0
  49. superred-0.1.0/src/superred/core/llm.py +166 -0
  50. superred-0.1.0/src/superred/core/middleware.py +114 -0
  51. superred-0.1.0/src/superred/core/persistence.py +501 -0
  52. superred-0.1.0/src/superred/core/types/__init__.py +71 -0
  53. superred-0.1.0/src/superred/core/types/controllable.py +31 -0
  54. superred-0.1.0/src/superred/core/types/evaluation.py +68 -0
  55. superred-0.1.0/src/superred/core/types/event.py +63 -0
  56. superred-0.1.0/src/superred/core/types/events.py +187 -0
  57. superred-0.1.0/src/superred/core/types/goal.py +16 -0
  58. superred-0.1.0/src/superred/core/types/llm.py +64 -0
  59. superred-0.1.0/src/superred/core/types/observable.py +44 -0
  60. superred-0.1.0/src/superred/core/types/security_domain.py +156 -0
  61. superred-0.1.0/src/superred/core/types/state.py +73 -0
  62. superred-0.1.0/src/superred/core/types/trajectory.py +167 -0
  63. superred-0.1.0/src/superred/py.typed +0 -0
  64. superred-0.1.0/tests/__init__.py +0 -0
  65. superred-0.1.0/tests/conftest.py +341 -0
  66. superred-0.1.0/tests/test_channel.py +337 -0
  67. superred-0.1.0/tests/test_controller.py +2080 -0
  68. superred-0.1.0/tests/test_controller_persistence.py +628 -0
  69. superred-0.1.0/tests/test_distinct_combinations.py +110 -0
  70. superred-0.1.0/tests/test_dynamic_scope.py +1006 -0
  71. superred-0.1.0/tests/test_evaluation_result_validation.py +46 -0
  72. superred-0.1.0/tests/test_integration.py +998 -0
  73. superred-0.1.0/tests/test_llm.py +696 -0
  74. superred-0.1.0/tests/test_middleware.py +257 -0
  75. superred-0.1.0/tests/test_mutations.py +1027 -0
  76. superred-0.1.0/tests/test_optimizer.py +267 -0
  77. superred-0.1.0/tests/test_persistence.py +669 -0
  78. superred-0.1.0/tests/test_properties.py +204 -0
  79. superred-0.1.0/tests/test_security_claim.py +102 -0
  80. superred-0.1.0/tests/test_security_domain.py +224 -0
  81. superred-0.1.0/tests/test_trajectory.py +398 -0
  82. 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.
@@ -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.