sondera-harness 0.6.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.
- sondera/__init__.py +111 -0
- sondera/__main__.py +4 -0
- sondera/adk/__init__.py +3 -0
- sondera/adk/analyze.py +222 -0
- sondera/adk/plugin.py +387 -0
- sondera/cli.py +22 -0
- sondera/exceptions.py +167 -0
- sondera/harness/__init__.py +6 -0
- sondera/harness/abc.py +102 -0
- sondera/harness/cedar/__init__.py +0 -0
- sondera/harness/cedar/harness.py +363 -0
- sondera/harness/cedar/schema.py +225 -0
- sondera/harness/sondera/__init__.py +0 -0
- sondera/harness/sondera/_grpc.py +354 -0
- sondera/harness/sondera/harness.py +890 -0
- sondera/langgraph/__init__.py +15 -0
- sondera/langgraph/analyze.py +543 -0
- sondera/langgraph/exceptions.py +19 -0
- sondera/langgraph/graph.py +210 -0
- sondera/langgraph/middleware.py +454 -0
- sondera/proto/google/protobuf/any_pb2.py +37 -0
- sondera/proto/google/protobuf/any_pb2.pyi +14 -0
- sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/duration_pb2.py +37 -0
- sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
- sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/empty_pb2.py +37 -0
- sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
- sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/struct_pb2.py +47 -0
- sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
- sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
- sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
- sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
- sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
- sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
- sondera/proto/sondera/__init__.py +0 -0
- sondera/proto/sondera/core/__init__.py +0 -0
- sondera/proto/sondera/core/v1/__init__.py +0 -0
- sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
- sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
- sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
- sondera/proto/sondera/harness/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
- sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
- sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
- sondera/py.typed +0 -0
- sondera/settings.py +20 -0
- sondera/strands/__init__.py +5 -0
- sondera/strands/analyze.py +244 -0
- sondera/strands/harness.py +333 -0
- sondera/tui/__init__.py +0 -0
- sondera/tui/app.py +309 -0
- sondera/tui/screens/__init__.py +5 -0
- sondera/tui/screens/adjudication.py +184 -0
- sondera/tui/screens/agent.py +158 -0
- sondera/tui/screens/trajectory.py +158 -0
- sondera/tui/widgets/__init__.py +23 -0
- sondera/tui/widgets/agent_card.py +94 -0
- sondera/tui/widgets/agent_list.py +73 -0
- sondera/tui/widgets/recent_adjudications.py +52 -0
- sondera/tui/widgets/recent_trajectories.py +54 -0
- sondera/tui/widgets/summary.py +57 -0
- sondera/tui/widgets/tool_card.py +33 -0
- sondera/tui/widgets/violation_panel.py +72 -0
- sondera/tui/widgets/violations_list.py +78 -0
- sondera/tui/widgets/violations_summary.py +104 -0
- sondera/types.py +346 -0
- sondera_harness-0.6.0.dist-info/METADATA +323 -0
- sondera_harness-0.6.0.dist-info/RECORD +77 -0
- sondera_harness-0.6.0.dist-info/WHEEL +5 -0
- sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
- sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
- sondera_harness-0.6.0.dist-info/top_level.txt +1 -0
sondera/adk/plugin.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sondera Harness Plugin for Google ADK integration.
|
|
3
|
+
|
|
4
|
+
This plugin implements the ADK BasePlugin callback patterns for policy enforcement,
|
|
5
|
+
guardrails, and security controls across agent workflows using the Sondera Harness.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from google.adk.agents.base_agent import BaseAgent
|
|
12
|
+
from google.adk.agents.callback_context import CallbackContext
|
|
13
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
14
|
+
from google.adk.agents.llm_agent import LlmAgent
|
|
15
|
+
from google.adk.events.event import Event
|
|
16
|
+
from google.adk.models.llm_request import LlmRequest
|
|
17
|
+
from google.adk.models.llm_response import LlmResponse
|
|
18
|
+
from google.adk.plugins.base_plugin import BasePlugin
|
|
19
|
+
from google.adk.tools.base_tool import BaseTool
|
|
20
|
+
from google.adk.tools.tool_context import ToolContext
|
|
21
|
+
from google.genai import types as genai_types
|
|
22
|
+
|
|
23
|
+
from sondera.adk.analyze import format
|
|
24
|
+
from sondera.harness import Harness
|
|
25
|
+
from sondera.types import (
|
|
26
|
+
PromptContent,
|
|
27
|
+
Role,
|
|
28
|
+
Stage,
|
|
29
|
+
ToolRequestContent,
|
|
30
|
+
ToolResponseContent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SonderaHarnessPlugin(BasePlugin):
|
|
37
|
+
"""Sondera Harness Plugin for ADK integration.
|
|
38
|
+
|
|
39
|
+
This plugin integrates with the Sondera Harness for policy enforcement,
|
|
40
|
+
guardrails, and governance across ADK agent workflows. It implements
|
|
41
|
+
the ADK BasePlugin interface and uses dependency injection for the
|
|
42
|
+
Harness instance.
|
|
43
|
+
|
|
44
|
+
The plugin intercepts agent execution at key points:
|
|
45
|
+
- User message: Initialize trajectory and evaluate user input
|
|
46
|
+
- Before/after model: Evaluate model requests and responses
|
|
47
|
+
- Before/after tool: Evaluate tool calls and results
|
|
48
|
+
- After run: Finalize the trajectory
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
```python
|
|
52
|
+
from sondera.adk import SonderaHarnessPlugin
|
|
53
|
+
from sondera.harness import RemoteHarness
|
|
54
|
+
from google.adk import Agent
|
|
55
|
+
from google.adk.runners import Runner
|
|
56
|
+
|
|
57
|
+
# Create a harness instance
|
|
58
|
+
harness = RemoteHarness(
|
|
59
|
+
sondera_harness_endpoint="localhost:50051",
|
|
60
|
+
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Create the plugin with the harness
|
|
64
|
+
plugin = SonderaHarnessPlugin(harness=harness)
|
|
65
|
+
|
|
66
|
+
# Create agent and runner with the plugin
|
|
67
|
+
agent = Agent(name="my-agent", model="gemini-2.0-flash", ...)
|
|
68
|
+
runner = Runner(
|
|
69
|
+
agent=agent,
|
|
70
|
+
app_name="my-app",
|
|
71
|
+
plugins=[plugin],
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
harness: Harness,
|
|
79
|
+
*,
|
|
80
|
+
logger_instance: logging.Logger | None = None,
|
|
81
|
+
):
|
|
82
|
+
"""Initialize the Sondera Harness Plugin.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
harness: The Sondera Harness instance to use for policy enforcement.
|
|
86
|
+
Can be RemoteHarness for production or LocalHarness for testing.
|
|
87
|
+
logger_instance: Optional custom logger instance.
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(name="sondera_harness")
|
|
90
|
+
self._harness = harness
|
|
91
|
+
self._log = logger_instance or logger
|
|
92
|
+
|
|
93
|
+
# -------------------------------------------------------------------------
|
|
94
|
+
# User Message Callback
|
|
95
|
+
# -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
async def on_user_message_callback(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
invocation_context: InvocationContext,
|
|
101
|
+
user_message: genai_types.Content,
|
|
102
|
+
) -> genai_types.Content | None:
|
|
103
|
+
"""Callback executed when a user message is received.
|
|
104
|
+
|
|
105
|
+
Initializes the harness trajectory and evaluates the user input
|
|
106
|
+
against policies before the agent processes it.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
invocation_context: The context for the entire invocation.
|
|
110
|
+
user_message: The message content input by user.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Modified content if policy violation, None to proceed normally.
|
|
114
|
+
"""
|
|
115
|
+
# Initialize trajectory with agent metadata
|
|
116
|
+
agent = format(
|
|
117
|
+
cast(LlmAgent, invocation_context.agent),
|
|
118
|
+
invocation_context.app_name,
|
|
119
|
+
invocation_context.app_name,
|
|
120
|
+
)
|
|
121
|
+
await self._harness.initialize(agent=agent)
|
|
122
|
+
|
|
123
|
+
# Extract text content from user message
|
|
124
|
+
content = None
|
|
125
|
+
if user_message.parts is not None:
|
|
126
|
+
content = user_message.parts[-1].text
|
|
127
|
+
if not content:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# Adjudicate user input
|
|
131
|
+
adjudication = await self._harness.adjudicate(
|
|
132
|
+
Stage.PRE_MODEL, Role.USER, PromptContent(text=content)
|
|
133
|
+
)
|
|
134
|
+
self._log.info(
|
|
135
|
+
f"[SonderaHarness] User message adjudication for trajectory {self._harness.trajectory_id}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if adjudication.is_denied:
|
|
139
|
+
return genai_types.Content(
|
|
140
|
+
parts=[genai_types.Part(text=adjudication.reason)]
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# -------------------------------------------------------------------------
|
|
145
|
+
# Agent Callbacks
|
|
146
|
+
# -------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
async def before_agent_callback(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
agent: BaseAgent,
|
|
152
|
+
callback_context: CallbackContext,
|
|
153
|
+
) -> genai_types.Content | None:
|
|
154
|
+
"""Callback executed before an agent's primary logic is invoked.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
agent: The agent that is about to run.
|
|
158
|
+
callback_context: The context for the agent invocation.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
None to allow agent to proceed normally.
|
|
162
|
+
"""
|
|
163
|
+
self._log.debug(f"[SonderaHarness] Before agent: {agent.name}")
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
async def after_agent_callback(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
agent: BaseAgent,
|
|
170
|
+
callback_context: CallbackContext,
|
|
171
|
+
) -> genai_types.Content | None:
|
|
172
|
+
"""Callback executed after an agent's primary logic has completed.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
agent: The agent that has just run.
|
|
176
|
+
callback_context: The context for the agent invocation.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
None to use original agent response.
|
|
180
|
+
"""
|
|
181
|
+
self._log.debug(f"[SonderaHarness] After agent: {agent.name}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
# -------------------------------------------------------------------------
|
|
185
|
+
# Model Callbacks
|
|
186
|
+
# -------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
async def before_model_callback(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
callback_context: CallbackContext,
|
|
192
|
+
llm_request: LlmRequest,
|
|
193
|
+
) -> LlmResponse | None:
|
|
194
|
+
"""Callback executed before a request is sent to the model.
|
|
195
|
+
|
|
196
|
+
Evaluates the model request against policies.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
callback_context: The context for the current agent call.
|
|
200
|
+
llm_request: The prepared request object to be sent to the model.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
LlmResponse if policy violation, None to proceed normally.
|
|
204
|
+
"""
|
|
205
|
+
self._log.debug(
|
|
206
|
+
f"[SonderaHarness] Before model call for trajectory {self._harness.trajectory_id}"
|
|
207
|
+
)
|
|
208
|
+
adjudication = await self._harness.adjudicate(
|
|
209
|
+
Stage.PRE_MODEL, Role.MODEL, PromptContent(text="")
|
|
210
|
+
)
|
|
211
|
+
self._log.info(
|
|
212
|
+
f"[SonderaHarness] Before model adjudication for trajectory {self._harness.trajectory_id}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if adjudication.is_denied:
|
|
216
|
+
return LlmResponse(
|
|
217
|
+
content=genai_types.Content(
|
|
218
|
+
parts=[genai_types.Part(text=adjudication.reason)]
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
async def after_model_callback(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
callback_context: CallbackContext,
|
|
227
|
+
llm_response: LlmResponse,
|
|
228
|
+
) -> LlmResponse | None:
|
|
229
|
+
"""Callback executed after a response is received from the model.
|
|
230
|
+
|
|
231
|
+
Evaluates the model response against policies.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
callback_context: The context for the current agent call.
|
|
235
|
+
llm_response: The response object received from the model.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Modified LlmResponse if policy violation, None to use original.
|
|
239
|
+
"""
|
|
240
|
+
self._log.debug("[SonderaHarness] After model call")
|
|
241
|
+
|
|
242
|
+
# Extract text content from response
|
|
243
|
+
content = None
|
|
244
|
+
if llm_response.content is not None and llm_response.content.parts is not None:
|
|
245
|
+
content = llm_response.content.parts[-1].text
|
|
246
|
+
|
|
247
|
+
if not content:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
adjudication = await self._harness.adjudicate(
|
|
251
|
+
Stage.POST_MODEL, Role.MODEL, PromptContent(text=content)
|
|
252
|
+
)
|
|
253
|
+
self._log.info(
|
|
254
|
+
f"[SonderaHarness] After model adjudication for trajectory {self._harness.trajectory_id}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if adjudication.is_denied:
|
|
258
|
+
return LlmResponse(
|
|
259
|
+
content=genai_types.Content(
|
|
260
|
+
parts=[genai_types.Part(text=adjudication.reason)]
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
# -------------------------------------------------------------------------
|
|
266
|
+
# Tool Callbacks
|
|
267
|
+
# -------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
async def before_tool_callback(
|
|
270
|
+
self,
|
|
271
|
+
*,
|
|
272
|
+
tool: BaseTool,
|
|
273
|
+
tool_args: dict[str, Any],
|
|
274
|
+
tool_context: ToolContext,
|
|
275
|
+
) -> dict[str, Any] | None:
|
|
276
|
+
"""Callback executed before a tool is called.
|
|
277
|
+
|
|
278
|
+
Evaluates the tool request against policies.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
tool: The tool instance that is about to be executed.
|
|
282
|
+
tool_args: The dictionary of arguments for the tool.
|
|
283
|
+
tool_context: The context specific to the tool execution.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict result if policy violation (stops tool), None to proceed.
|
|
287
|
+
"""
|
|
288
|
+
self._log.debug(f"[SonderaHarness] Before tool: {tool.name}")
|
|
289
|
+
|
|
290
|
+
adjudication = await self._harness.adjudicate(
|
|
291
|
+
Stage.PRE_TOOL,
|
|
292
|
+
Role.TOOL,
|
|
293
|
+
ToolRequestContent(tool_id=tool.name, args=tool_args),
|
|
294
|
+
)
|
|
295
|
+
self._log.info(
|
|
296
|
+
f"[SonderaHarness] Before tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if adjudication.is_denied:
|
|
300
|
+
return {"error": f"Tool blocked: {adjudication.reason}"}
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
async def after_tool_callback(
|
|
304
|
+
self,
|
|
305
|
+
*,
|
|
306
|
+
tool: BaseTool,
|
|
307
|
+
tool_args: dict[str, Any],
|
|
308
|
+
tool_context: ToolContext,
|
|
309
|
+
result: dict[str, Any],
|
|
310
|
+
) -> dict[str, Any] | None:
|
|
311
|
+
"""Callback executed after a tool has been called.
|
|
312
|
+
|
|
313
|
+
Evaluates the tool result against policies.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
tool: The tool instance that has just been executed.
|
|
317
|
+
tool_args: The original arguments passed to the tool.
|
|
318
|
+
tool_context: The context specific to the tool execution.
|
|
319
|
+
result: The dictionary returned by the tool invocation.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Modified result dict if policy violation, None to use original.
|
|
323
|
+
"""
|
|
324
|
+
self._log.debug(f"[SonderaHarness] After tool: {tool.name}")
|
|
325
|
+
|
|
326
|
+
adjudication = await self._harness.adjudicate(
|
|
327
|
+
Stage.POST_TOOL,
|
|
328
|
+
Role.TOOL,
|
|
329
|
+
ToolResponseContent(tool_id=tool.name, response=result),
|
|
330
|
+
)
|
|
331
|
+
self._log.info(
|
|
332
|
+
f"[SonderaHarness] After tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if adjudication.is_denied:
|
|
336
|
+
return {"error": f"Tool result blocked: {adjudication.reason}"}
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
# -------------------------------------------------------------------------
|
|
340
|
+
# Event Callback
|
|
341
|
+
# -------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
async def on_event_callback(
|
|
344
|
+
self,
|
|
345
|
+
*,
|
|
346
|
+
invocation_context: InvocationContext,
|
|
347
|
+
event: Event,
|
|
348
|
+
) -> Event | None:
|
|
349
|
+
"""Callback executed after an event is yielded from runner.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
invocation_context: The context for the entire invocation.
|
|
353
|
+
event: The event raised by the runner.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
None to use original event.
|
|
357
|
+
"""
|
|
358
|
+
self._log.debug(f"[SonderaHarness] Event: {event.author}")
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
# -------------------------------------------------------------------------
|
|
362
|
+
# Runner Lifecycle Callbacks
|
|
363
|
+
# -------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
async def after_run_callback(
|
|
366
|
+
self,
|
|
367
|
+
*,
|
|
368
|
+
invocation_context: InvocationContext,
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Callback executed after an ADK runner run has completed.
|
|
371
|
+
|
|
372
|
+
Finalizes the harness trajectory.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
invocation_context: The context for the entire invocation.
|
|
376
|
+
"""
|
|
377
|
+
self._log.info(
|
|
378
|
+
f"[SonderaHarness] Finalizing trajectory {self._harness.trajectory_id}"
|
|
379
|
+
)
|
|
380
|
+
await self._harness.finalize()
|
|
381
|
+
|
|
382
|
+
async def close(self) -> None:
|
|
383
|
+
"""Method executed when the runner is closed.
|
|
384
|
+
|
|
385
|
+
Used for cleanup tasks such as closing network connections.
|
|
386
|
+
"""
|
|
387
|
+
self._log.debug("[SonderaHarness] Plugin closed")
|
sondera/cli.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""The Sondera Harness CLI and TUI entrypoints."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click_default_group import DefaultGroup
|
|
5
|
+
|
|
6
|
+
from sondera.tui.app import SonderaApp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(cls=DefaultGroup, default="default", default_if_no_args=True)
|
|
10
|
+
def cli() -> None:
|
|
11
|
+
"""A CLI and TUI for interacting with the Sondera Harness SDK and Harness Service."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command()
|
|
15
|
+
def default() -> None:
|
|
16
|
+
"""Launch the Sondera Harness TUI."""
|
|
17
|
+
app = SonderaApp()
|
|
18
|
+
app.run()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
cli()
|
sondera/exceptions.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Sondera SDK exception hierarchy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sondera.types import Adjudication, Stage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SonderaError(Exception):
|
|
12
|
+
"""Base exception for all Sondera SDK errors."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationError(SonderaError):
|
|
18
|
+
"""Raised when there is a configuration error.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
- Missing required API key
|
|
22
|
+
- Invalid endpoint format
|
|
23
|
+
- Missing required settings
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthenticationError(SonderaError):
|
|
30
|
+
"""Raised when authentication fails.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
- Invalid or expired JWT token
|
|
34
|
+
- Missing authentication credentials
|
|
35
|
+
- Token lacks required claims
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConnectionError(SonderaError):
|
|
42
|
+
"""Raised when connection to the harness service fails.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
- Network unreachable
|
|
46
|
+
- Service unavailable
|
|
47
|
+
- TLS handshake failure
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TrajectoryError(SonderaError):
|
|
54
|
+
"""Raised for trajectory-related errors.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
- Trajectory not initialized
|
|
58
|
+
- Invalid trajectory state
|
|
59
|
+
- Trajectory already finalized
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TrajectoryNotInitializedError(TrajectoryError):
|
|
66
|
+
"""Raised when attempting operations without an active trajectory."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, message: str = "No active trajectory. Call initialize() first."):
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PolicyError(SonderaError):
|
|
73
|
+
"""Base exception for policy-related errors."""
|
|
74
|
+
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PolicyViolationError(PolicyError):
|
|
79
|
+
"""Raised when a policy violation blocks execution.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
stage: The execution stage where the violation occurred
|
|
83
|
+
reason: The policy violation reason
|
|
84
|
+
adjudication: The full adjudication result
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
stage: Stage,
|
|
90
|
+
reason: str,
|
|
91
|
+
adjudication: Adjudication | None = None,
|
|
92
|
+
):
|
|
93
|
+
self.stage = stage
|
|
94
|
+
self.reason = reason
|
|
95
|
+
self.adjudication = adjudication
|
|
96
|
+
super().__init__(f"Policy violation at {stage.value}: {reason}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PolicyEvaluationError(PolicyError):
|
|
100
|
+
"""Raised when policy evaluation fails.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
- Invalid policy syntax
|
|
104
|
+
- Schema mismatch
|
|
105
|
+
- Policy engine internal error
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AgentError(SonderaError):
|
|
112
|
+
"""Raised for agent-related errors.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
- Invalid agent configuration
|
|
116
|
+
- Agent not found
|
|
117
|
+
- Agent registration failed
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ToolError(SonderaError):
|
|
124
|
+
"""Raised for tool-related errors.
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
- Tool not found
|
|
128
|
+
- Invalid tool arguments
|
|
129
|
+
- Tool execution blocked
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
tool_name: str,
|
|
135
|
+
message: str,
|
|
136
|
+
*,
|
|
137
|
+
tool_args: dict | None = None,
|
|
138
|
+
):
|
|
139
|
+
self.tool_name = tool_name
|
|
140
|
+
self.tool_args = tool_args
|
|
141
|
+
super().__init__(f"Tool '{tool_name}': {message}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ToolBlockedError(ToolError):
|
|
145
|
+
"""Raised when a tool execution is blocked by policy."""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
tool_name: str,
|
|
150
|
+
reason: str,
|
|
151
|
+
*,
|
|
152
|
+
tool_args: dict | None = None,
|
|
153
|
+
):
|
|
154
|
+
self.reason = reason
|
|
155
|
+
super().__init__(tool_name, f"Blocked - {reason}", tool_args=tool_args)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SerializationError(SonderaError):
|
|
159
|
+
"""Raised when serialization or deserialization fails.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
- Invalid protobuf message
|
|
163
|
+
- JSON encoding error
|
|
164
|
+
- Type conversion failure
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
pass
|
sondera/harness/abc.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from sondera.types import Adjudication, Agent, Content, Role, Stage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Harness(ABC):
|
|
7
|
+
"""Abstract base class defining the interface for Sondera Harness implementations.
|
|
8
|
+
|
|
9
|
+
This ABC defines the core contract for harness implementations that integrate
|
|
10
|
+
with the Sondera Platform for agent governance, trajectory management, and
|
|
11
|
+
real-time step adjudication.
|
|
12
|
+
|
|
13
|
+
Subclasses must implement:
|
|
14
|
+
- resume: Resume an existing trajectory for continued execution
|
|
15
|
+
- initialize: Set up a new trajectory for agent execution
|
|
16
|
+
- finalize: Complete and save the current trajectory
|
|
17
|
+
- adjudicate: Evaluate a trajectory step against policies
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
trajectory_id: The current active trajectory ID (None if no active trajectory)
|
|
21
|
+
agent: The agent being governed (may be None until initialize is called)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_trajectory_id: str | None
|
|
25
|
+
_agent: Agent | None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def trajectory_id(self) -> str | None:
|
|
29
|
+
"""Get the current trajectory ID."""
|
|
30
|
+
return self._trajectory_id
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def agent(self) -> Agent | None:
|
|
34
|
+
"""Get the current agent."""
|
|
35
|
+
return self._agent
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def resume(self, trajectory_id, *, agent: Agent | None = None) -> None:
|
|
39
|
+
"""Resume an existing trajectory for the given agent."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def initialize(self, *, agent: Agent | None = None) -> None:
|
|
44
|
+
"""Initialize a new trajectory for the current execution.
|
|
45
|
+
|
|
46
|
+
This method should:
|
|
47
|
+
1. Register the agent with the Sondera Platform if not already registered
|
|
48
|
+
2. Create a new trajectory for tracking the agent's execution
|
|
49
|
+
3. Store the trajectory ID for subsequent adjudication calls
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
agent: Optional agent to use for this trajectory. If provided, overrides
|
|
53
|
+
any agent set during construction.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If no agent is provided and none was set during construction
|
|
57
|
+
RuntimeError: If connection to the harness service fails
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def finalize(self) -> None:
|
|
63
|
+
"""Finalize the current trajectory and save artifacts.
|
|
64
|
+
|
|
65
|
+
This method should:
|
|
66
|
+
1. Mark the trajectory as completed
|
|
67
|
+
2. Persist any remaining trajectory data
|
|
68
|
+
3. Clear the active trajectory state
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If no active trajectory exists (initialize not called)
|
|
72
|
+
RuntimeError: If finalization fails
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def adjudicate(
|
|
78
|
+
self,
|
|
79
|
+
stage: Stage,
|
|
80
|
+
role: Role,
|
|
81
|
+
content: Content,
|
|
82
|
+
) -> Adjudication:
|
|
83
|
+
"""Adjudicate a trajectory step using the policy engine.
|
|
84
|
+
|
|
85
|
+
Evaluates the given step against configured policies and returns
|
|
86
|
+
an adjudication decision (ALLOW, DENY, or ESCALATE).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
stage: The execution stage (PRE_RUN, PRE_MODEL, POST_MODEL,
|
|
90
|
+
PRE_TOOL, POST_TOOL, POST_RUN)
|
|
91
|
+
role: The role of the actor (USER, MODEL, TOOL, SYSTEM)
|
|
92
|
+
content: The content to evaluate (PromptContent, ToolRequestContent,
|
|
93
|
+
or ToolResponseContent)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Adjudication containing the decision and reason
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RuntimeError: If no active trajectory exists (initialize not called)
|
|
100
|
+
ValueError: If the content type is not supported
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
File without changes
|