superred 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
superred/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """superred — A modular framework for comprehensive red-teaming of AI systems."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,111 @@
1
+ """Core interfaces and types for the superred framework."""
2
+
3
+ from superred.core.channel import EventChannel, EventEnvelope
4
+ from superred.core.controller import (
5
+ Controller,
6
+ OptimizerFactory,
7
+ RunResult,
8
+ ScopeResolver,
9
+ TargetFactory,
10
+ TaskResult,
11
+ ThreatModelResult,
12
+ )
13
+ from superred.core.interfaces import (
14
+ NotApplicable,
15
+ Optimizer,
16
+ SecurityClaim,
17
+ Target,
18
+ Task,
19
+ )
20
+ from superred.core.middleware import (
21
+ Middleware,
22
+ compose,
23
+ security_domain_filter,
24
+ trajectory_recorder,
25
+ )
26
+ from superred.core.types import (
27
+ ConfigSpec,
28
+ Controllable,
29
+ ControllableInjection,
30
+ ControllableNoInjection,
31
+ ControllablePostCallEvent,
32
+ ControllablePreCallEvent,
33
+ EvaluationResult,
34
+ Event,
35
+ EventHandler,
36
+ EventResponse,
37
+ EventResponseHandler,
38
+ FilteredTrajectory,
39
+ Goal,
40
+ Observable,
41
+ ObservableEvent,
42
+ ObservableValue,
43
+ QueryParam,
44
+ QuerySpec,
45
+ ReadableTrajectory,
46
+ RunEndEvent,
47
+ RunEndResponse,
48
+ RunStartEvent,
49
+ Scope,
50
+ Score,
51
+ SecurityDomain,
52
+ SecurityDomainTag,
53
+ Trajectory,
54
+ get_domain,
55
+ scope_includes,
56
+ )
57
+
58
+ __all__ = [
59
+ # Channel
60
+ "EventChannel",
61
+ "EventEnvelope",
62
+ # Controller
63
+ "Controller",
64
+ "OptimizerFactory",
65
+ "RunResult",
66
+ "ScopeResolver",
67
+ "TargetFactory",
68
+ "TaskResult",
69
+ "ThreatModelResult",
70
+ # Middleware
71
+ "Middleware",
72
+ "compose",
73
+ "security_domain_filter",
74
+ "trajectory_recorder",
75
+ # Interfaces
76
+ "NotApplicable",
77
+ "Optimizer",
78
+ "SecurityClaim",
79
+ "Target",
80
+ "Task",
81
+ # Types
82
+ "ConfigSpec",
83
+ "Controllable",
84
+ "ControllableInjection",
85
+ "ControllableNoInjection",
86
+ "ControllablePostCallEvent",
87
+ "ControllablePreCallEvent",
88
+ "EvaluationResult",
89
+ "Event",
90
+ "EventHandler",
91
+ "EventResponse",
92
+ "EventResponseHandler",
93
+ "FilteredTrajectory",
94
+ "Goal",
95
+ "ObservableEvent",
96
+ "Observable",
97
+ "ObservableValue",
98
+ "QueryParam",
99
+ "QuerySpec",
100
+ "ReadableTrajectory",
101
+ "RunEndEvent",
102
+ "RunEndResponse",
103
+ "RunStartEvent",
104
+ "Scope",
105
+ "Score",
106
+ "SecurityDomain",
107
+ "SecurityDomainTag",
108
+ "Trajectory",
109
+ "get_domain",
110
+ "scope_includes",
111
+ ]
@@ -0,0 +1,227 @@
1
+ """Event channel: thread-safe bidirectional event-response communication.
2
+
3
+ The channel decouples event senders (controller/target side) from receivers
4
+ (optimizer side). The sender puts an event and awaits a response. The
5
+ receiver pulls events at its own pace and responds when ready.
6
+
7
+ Error propagation:
8
+ If the receiver encounters an error, it calls ``envelope.reject(exc)``
9
+ to propagate the exception to the sender. If the receiver task dies,
10
+ ``channel.set_error(exc)`` poisons the channel — all pending and
11
+ future ``send()`` calls raise the exception.
12
+
13
+ Thread safety:
14
+ - ``send()`` and ``receive()`` run on the asyncio event loop thread.
15
+ - ``respond()``, ``reject()``, ``close()``, and ``set_error()`` use
16
+ ``call_soon_threadsafe`` so they can be called safely from any thread.
17
+ - The internal ``asyncio.Queue`` is only accessed from the event loop.
18
+
19
+ Process safety:
20
+ This is an in-process implementation. The interface (send/receive/
21
+ respond/close) is designed so a future process-safe implementation
22
+ (multiprocessing pipes, sockets, etc.) can provide the same contract.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ import threading
29
+ from collections.abc import AsyncIterator
30
+
31
+ from superred.core.types.event import Event, EventResponse
32
+
33
+
34
+ class EventEnvelope:
35
+ """An event paired with its response mechanism.
36
+
37
+ The receiver calls :meth:`respond` exactly once to deliver a successful
38
+ response, or :meth:`reject` to propagate an exception to the sender.
39
+ Thread-safe — both methods may be called from any thread.
40
+
41
+ Attributes:
42
+ event: The event to handle.
43
+ """
44
+
45
+ __slots__ = ("event", "_future", "_loop", "_responded", "_lock")
46
+
47
+ def __init__(
48
+ self,
49
+ event: Event,
50
+ future: asyncio.Future[EventResponse],
51
+ loop: asyncio.AbstractEventLoop,
52
+ ) -> None:
53
+ self.event = event
54
+ self._future = future
55
+ self._loop = loop
56
+ self._responded = False
57
+ self._lock = threading.Lock()
58
+
59
+ def respond(self, response: EventResponse) -> None:
60
+ """Deliver a successful response for this event.
61
+
62
+ Thread-safe. Must be called exactly once (mutually exclusive with
63
+ :meth:`reject`). The response type is validated against the
64
+ event's ``response_types`` declaration.
65
+
66
+ Raises:
67
+ RuntimeError: If called more than once or after reject.
68
+ TypeError: If the response type is not allowed for this event.
69
+ """
70
+ # Validate response type against event's declared response_types.
71
+ # Empty tuple = any EventResponse accepted (base Event default).
72
+ allowed = self.event.response_types
73
+ if allowed and not isinstance(response, allowed):
74
+ allowed_names = ", ".join(t.__name__ for t in allowed)
75
+ error = TypeError(
76
+ f"{type(self.event).__name__} requires response of type "
77
+ f"{allowed_names}, got {type(response).__name__}"
78
+ )
79
+ # Reject the envelope so the sender doesn't deadlock
80
+ self.reject(error)
81
+ raise error
82
+ with self._lock:
83
+ if self._responded:
84
+ raise RuntimeError("EventEnvelope already responded/rejected")
85
+ self._responded = True
86
+ self._loop.call_soon_threadsafe(self._future.set_result, response)
87
+
88
+ def reject(self, error: BaseException) -> None:
89
+ """Propagate an exception to the sender.
90
+
91
+ Thread-safe. Must be called at most once (mutually exclusive with
92
+ :meth:`respond`). The sender's ``await channel.send(...)`` will
93
+ raise this exception.
94
+
95
+ If already responded/rejected, this is a no-op (safe to call as
96
+ cleanup in error paths).
97
+ """
98
+ with self._lock:
99
+ if self._responded:
100
+ return
101
+ self._responded = True
102
+ self._loop.call_soon_threadsafe(self._future.set_exception, error)
103
+
104
+
105
+ class EventChannel:
106
+ """Thread-safe bidirectional event-response channel.
107
+
108
+ The send side puts events and awaits responses. The receive side
109
+ pulls events at its own pace and responds via the envelope.
110
+
111
+ Supports ``async for`` iteration on the receive side::
112
+
113
+ async for envelope in channel:
114
+ response = handle(envelope.event)
115
+ envelope.respond(response)
116
+
117
+ Iteration ends when the channel is closed and all envelopes have
118
+ been consumed.
119
+
120
+ Error propagation:
121
+ Call :meth:`set_error` to poison the channel. All pending
122
+ ``send()`` futures are resolved with the exception, and future
123
+ ``send()`` calls raise it immediately.
124
+ """
125
+
126
+ def __init__(self) -> None:
127
+ self._queue: asyncio.Queue[EventEnvelope | None] = asyncio.Queue()
128
+ self._loop: asyncio.AbstractEventLoop | None = None
129
+ self._closed = False
130
+ self._error: BaseException | None = None
131
+ self._close_lock = threading.Lock()
132
+
133
+ async def send(self, event: Event) -> EventResponse:
134
+ """Send an event and wait for its response.
135
+
136
+ Must be called from the event loop thread (i.e., from an async
137
+ context on the loop that the channel is bound to).
138
+
139
+ Args:
140
+ event: The event to send.
141
+
142
+ Returns:
143
+ The response produced by the receiver.
144
+
145
+ Raises:
146
+ RuntimeError: If the channel is already closed.
147
+ Exception: If the channel has been poisoned via :meth:`set_error`,
148
+ re-raises the stored exception.
149
+ """
150
+ if self._error is not None:
151
+ raise self._error
152
+ if self._closed:
153
+ raise RuntimeError("Cannot send on a closed EventChannel")
154
+ loop = asyncio.get_running_loop()
155
+ if self._loop is None:
156
+ self._loop = loop
157
+ future: asyncio.Future[EventResponse] = loop.create_future()
158
+ envelope = EventEnvelope(event=event, future=future, loop=loop)
159
+ await self._queue.put(envelope)
160
+ return await future
161
+
162
+ async def receive(self) -> EventEnvelope | None:
163
+ """Receive the next event envelope.
164
+
165
+ Returns ``None`` when the channel has been closed and all pending
166
+ envelopes have been consumed.
167
+
168
+ Must be called from the event loop thread.
169
+ """
170
+ if self._loop is None:
171
+ self._loop = asyncio.get_running_loop()
172
+ return await self._queue.get()
173
+
174
+ def close(self) -> None:
175
+ """Signal that no more events will be sent.
176
+
177
+ Thread-safe — may be called from any thread. Puts a sentinel
178
+ (``None``) on the queue so the receiver knows to stop.
179
+ """
180
+ with self._close_lock:
181
+ if self._closed:
182
+ return
183
+ self._closed = True
184
+ self._put_sentinel()
185
+
186
+ def set_error(self, error: BaseException) -> None:
187
+ """Poison the channel with an exception.
188
+
189
+ Thread-safe. All queued envelopes have their futures rejected
190
+ with the exception. Future ``send()`` calls raise it immediately.
191
+ The channel is also closed (receiver gets sentinel).
192
+
193
+ Idempotent — second call is a no-op.
194
+ """
195
+ with self._close_lock:
196
+ if self._closed:
197
+ return
198
+ self._error = error
199
+ self._closed = True
200
+ # Drain queued envelopes — reject their pending futures
201
+ while True:
202
+ try:
203
+ item = self._queue.get_nowait()
204
+ except asyncio.QueueEmpty:
205
+ break
206
+ if item is not None:
207
+ item.reject(error)
208
+ self._put_sentinel()
209
+
210
+ def _put_sentinel(self) -> None:
211
+ """Put the close sentinel on the queue."""
212
+ loop = self._loop
213
+ if loop is not None:
214
+ loop.call_soon_threadsafe(self._queue.put_nowait, None)
215
+ else:
216
+ self._queue.put_nowait(None)
217
+
218
+ # -- Async iteration ---------------------------------------------------
219
+
220
+ def __aiter__(self) -> AsyncIterator[EventEnvelope]:
221
+ return self
222
+
223
+ async def __anext__(self) -> EventEnvelope:
224
+ envelope = await self.receive()
225
+ if envelope is None:
226
+ raise StopAsyncIteration
227
+ return envelope