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 +3 -0
- superred/core/__init__.py +111 -0
- superred/core/channel.py +227 -0
- superred/core/controller.py +1056 -0
- superred/core/interfaces/__init__.py +14 -0
- superred/core/interfaces/optimizer.py +240 -0
- superred/core/interfaces/security_claim.py +76 -0
- superred/core/interfaces/target.py +180 -0
- superred/core/interfaces/task.py +113 -0
- superred/core/llm.py +166 -0
- superred/core/middleware.py +114 -0
- superred/core/persistence.py +501 -0
- superred/core/types/__init__.py +71 -0
- superred/core/types/controllable.py +31 -0
- superred/core/types/evaluation.py +68 -0
- superred/core/types/event.py +63 -0
- superred/core/types/events.py +187 -0
- superred/core/types/goal.py +16 -0
- superred/core/types/llm.py +64 -0
- superred/core/types/observable.py +44 -0
- superred/core/types/security_domain.py +156 -0
- superred/core/types/state.py +73 -0
- superred/core/types/trajectory.py +167 -0
- superred/py.typed +0 -0
- superred-0.1.0.dist-info/METADATA +175 -0
- superred-0.1.0.dist-info/RECORD +28 -0
- superred-0.1.0.dist-info/WHEEL +4 -0
- superred-0.1.0.dist-info/licenses/LICENSE +21 -0
superred/__init__.py
ADDED
|
@@ -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
|
+
]
|
superred/core/channel.py
ADDED
|
@@ -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
|