sondera-harness 0.6.2__py3-none-any.whl → 0.7.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 +35 -15
- sondera/adk/plugin.py +7 -6
- sondera/harness/cedar/harness.py +90 -19
- sondera/langgraph/middleware.py +6 -5
- sondera/strands/harness.py +5 -4
- sondera/types.py +4 -36
- sondera_harness-0.7.0.dist-info/METADATA +169 -0
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.7.0.dist-info}/RECORD +12 -12
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.7.0.dist-info}/WHEEL +1 -1
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.7.0.dist-info}/entry_points.txt +1 -0
- sondera_harness-0.6.2.dist-info/METADATA +0 -323
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.7.0.dist-info}/top_level.txt +0 -0
sondera/__init__.py
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
|
-
"""Sondera
|
|
1
|
+
"""Sondera Harness - Steer agents with rules, not prompts.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Wrap your agent, write Cedar policies, ship with confidence. When a policy
|
|
4
|
+
denies an action, the agent gets the reason why and adjusts. Agents self-correct
|
|
5
|
+
instead of failing. This is steering, not just blocking.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Same input, same verdict. Rules are deterministic, not probabilistic. Stop
|
|
8
|
+
debugging prompts and start writing policies.
|
|
9
|
+
|
|
10
|
+
Why Sondera Harness:
|
|
11
|
+
- Steer, don't just block: Denied actions include explanations
|
|
12
|
+
- Drop-in integration: Native middleware for LangGraph, ADK, Strands
|
|
13
|
+
- Full observability: Trajectories capture every action and decision
|
|
14
|
+
- Deterministic rules: Same input, same verdict, every time
|
|
15
|
+
- Ship faster: Reliability, safety, security, and compliance built in
|
|
16
|
+
|
|
17
|
+
Harness Implementations:
|
|
18
|
+
- CedarPolicyHarness: Local evaluation, no network calls, no dependencies
|
|
19
|
+
- SonderaRemoteHarness: Team policies, dashboards, centralized audit logs
|
|
10
20
|
|
|
11
21
|
Framework Integrations:
|
|
12
|
-
- sondera.langgraph: LangGraph
|
|
22
|
+
- sondera.langgraph: LangGraph middleware
|
|
13
23
|
- sondera.adk: Google ADK plugin
|
|
14
|
-
- sondera.strands: Strands
|
|
24
|
+
- sondera.strands: Strands lifecycle hooks
|
|
15
25
|
|
|
16
26
|
Example:
|
|
17
|
-
>>> from sondera import
|
|
18
|
-
>>> harness
|
|
27
|
+
>>> from sondera import CedarPolicyHarness, Agent, Tool
|
|
28
|
+
>>> from sondera.harness.cedar.schema import agent_to_cedar_schema
|
|
29
|
+
>>>
|
|
19
30
|
>>> agent = Agent(
|
|
20
31
|
... id="my-agent",
|
|
21
|
-
... provider_id="
|
|
22
|
-
... name="
|
|
32
|
+
... provider_id="local",
|
|
33
|
+
... name="My_Agent",
|
|
23
34
|
... description="A helpful assistant",
|
|
24
|
-
... instruction="
|
|
25
|
-
... tools=[],
|
|
35
|
+
... instruction="Help users with tasks",
|
|
36
|
+
... tools=[Tool(name="Bash", description="Run commands", parameters=[])],
|
|
37
|
+
... )
|
|
38
|
+
>>> policy = "permit(principal, action, resource);"
|
|
39
|
+
>>> harness = CedarPolicyHarness(
|
|
40
|
+
... policy_set=policy,
|
|
41
|
+
... schema=agent_to_cedar_schema(agent),
|
|
26
42
|
... )
|
|
27
43
|
>>> await harness.initialize(agent=agent)
|
|
28
44
|
"""
|
|
@@ -47,10 +63,12 @@ from sondera.types import (
|
|
|
47
63
|
AdjudicatedStep,
|
|
48
64
|
AdjudicatedTrajectory,
|
|
49
65
|
Adjudication,
|
|
66
|
+
AdjudicationRecord,
|
|
50
67
|
Agent,
|
|
51
68
|
Content,
|
|
52
69
|
Decision,
|
|
53
70
|
Parameter,
|
|
71
|
+
PolicyAnnotation,
|
|
54
72
|
PolicyEngineMode,
|
|
55
73
|
PromptContent,
|
|
56
74
|
Role,
|
|
@@ -93,6 +111,8 @@ __all__ = [
|
|
|
93
111
|
"Adjudication",
|
|
94
112
|
"AdjudicatedStep",
|
|
95
113
|
"AdjudicatedTrajectory",
|
|
114
|
+
"AdjudicationRecord",
|
|
115
|
+
"PolicyAnnotation",
|
|
96
116
|
"Decision",
|
|
97
117
|
# Exceptions
|
|
98
118
|
"SonderaError",
|
sondera/adk/plugin.py
CHANGED
|
@@ -23,6 +23,7 @@ from google.genai import types as genai_types
|
|
|
23
23
|
from sondera.adk.analyze import format
|
|
24
24
|
from sondera.harness import Harness
|
|
25
25
|
from sondera.types import (
|
|
26
|
+
Decision,
|
|
26
27
|
PromptContent,
|
|
27
28
|
Role,
|
|
28
29
|
Stage,
|
|
@@ -64,7 +65,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
64
65
|
plugin = SonderaHarnessPlugin(harness=harness)
|
|
65
66
|
|
|
66
67
|
# Create agent and runner with the plugin
|
|
67
|
-
agent = Agent(name="my-agent", model="gemini-2.
|
|
68
|
+
agent = Agent(name="my-agent", model="gemini-2.5-flash", ...)
|
|
68
69
|
runner = Runner(
|
|
69
70
|
agent=agent,
|
|
70
71
|
app_name="my-app",
|
|
@@ -135,7 +136,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
135
136
|
f"[SonderaHarness] User message adjudication for trajectory {self._harness.trajectory_id}"
|
|
136
137
|
)
|
|
137
138
|
|
|
138
|
-
if adjudication.
|
|
139
|
+
if adjudication.decision == Decision.DENY:
|
|
139
140
|
return genai_types.Content(
|
|
140
141
|
parts=[genai_types.Part(text=adjudication.reason)]
|
|
141
142
|
)
|
|
@@ -212,7 +213,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
212
213
|
f"[SonderaHarness] Before model adjudication for trajectory {self._harness.trajectory_id}"
|
|
213
214
|
)
|
|
214
215
|
|
|
215
|
-
if adjudication.
|
|
216
|
+
if adjudication.decision == Decision.DENY:
|
|
216
217
|
return LlmResponse(
|
|
217
218
|
content=genai_types.Content(
|
|
218
219
|
parts=[genai_types.Part(text=adjudication.reason)]
|
|
@@ -254,7 +255,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
254
255
|
f"[SonderaHarness] After model adjudication for trajectory {self._harness.trajectory_id}"
|
|
255
256
|
)
|
|
256
257
|
|
|
257
|
-
if adjudication.
|
|
258
|
+
if adjudication.decision == Decision.DENY:
|
|
258
259
|
return LlmResponse(
|
|
259
260
|
content=genai_types.Content(
|
|
260
261
|
parts=[genai_types.Part(text=adjudication.reason)]
|
|
@@ -296,7 +297,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
296
297
|
f"[SonderaHarness] Before tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
297
298
|
)
|
|
298
299
|
|
|
299
|
-
if adjudication.
|
|
300
|
+
if adjudication.decision == Decision.DENY:
|
|
300
301
|
return {"error": f"Tool blocked: {adjudication.reason}"}
|
|
301
302
|
return None
|
|
302
303
|
|
|
@@ -332,7 +333,7 @@ class SonderaHarnessPlugin(BasePlugin):
|
|
|
332
333
|
f"[SonderaHarness] After tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
333
334
|
)
|
|
334
335
|
|
|
335
|
-
if adjudication.
|
|
336
|
+
if adjudication.decision == Decision.DENY:
|
|
336
337
|
return {"error": f"Tool result blocked: {adjudication.reason}"}
|
|
337
338
|
return None
|
|
338
339
|
|
sondera/harness/cedar/harness.py
CHANGED
|
@@ -23,6 +23,7 @@ from sondera.types import (
|
|
|
23
23
|
Agent,
|
|
24
24
|
Content,
|
|
25
25
|
Decision,
|
|
26
|
+
PolicyAnnotation,
|
|
26
27
|
PromptContent,
|
|
27
28
|
Role,
|
|
28
29
|
Stage,
|
|
@@ -69,6 +70,7 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
69
70
|
*,
|
|
70
71
|
policy_set: PolicySet | str,
|
|
71
72
|
schema: CedarSchema,
|
|
73
|
+
agent: Agent | None = None,
|
|
72
74
|
logger: logging.Logger | None = None,
|
|
73
75
|
):
|
|
74
76
|
"""Initialize the Cedar policy engine.
|
|
@@ -77,12 +79,13 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
77
79
|
policy_set: Cedar policies to evaluate. Can be a PolicySet instance
|
|
78
80
|
or Cedar policy text. Required.
|
|
79
81
|
schema: Cedar schema generated from agent_to_cedar_schema(). Required.
|
|
82
|
+
agent: The agent to govern. Required for adjudication.
|
|
80
83
|
logger: Logger instance.
|
|
81
84
|
|
|
82
85
|
Raises:
|
|
83
86
|
ValueError: If policy_set or schema is not provided.
|
|
84
87
|
"""
|
|
85
|
-
self._agent: Agent | None =
|
|
88
|
+
self._agent: Agent | None = agent
|
|
86
89
|
self._trajectory_id: str | None = None
|
|
87
90
|
self._trajectory_step_count: int = 0
|
|
88
91
|
self._logger = logger or _LOGGER
|
|
@@ -101,6 +104,16 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
101
104
|
self._policy_set = PolicySet(policy_set)
|
|
102
105
|
else:
|
|
103
106
|
self._policy_set = policy_set
|
|
107
|
+
|
|
108
|
+
for policy in self._policy_set.policies():
|
|
109
|
+
annotations = policy.annotations()
|
|
110
|
+
if "escalate" in annotations and str(policy.effect()) != "Forbid":
|
|
111
|
+
policy_id = annotations.get("id", policy.id())
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Policy '{policy_id}' has @escalate but is not a forbid policy. "
|
|
114
|
+
"@escalate is only valid on forbid policies."
|
|
115
|
+
)
|
|
116
|
+
|
|
104
117
|
# Extract namespace name from schema
|
|
105
118
|
namespaces = list(schema.root.keys())
|
|
106
119
|
if namespaces:
|
|
@@ -110,6 +123,8 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
110
123
|
raise ValueError("Schema must have at least one namespace")
|
|
111
124
|
# Authorizer will be initialized with entities when agent is set
|
|
112
125
|
self._authorizer: Authorizer | None = None
|
|
126
|
+
# Cache for pre-parsed tool response schemas (tool_name -> parsed schema dict)
|
|
127
|
+
self._tool_response_schemas: dict[str, dict[str, object]] = {}
|
|
113
128
|
|
|
114
129
|
def _build_authorizer(self) -> Authorizer:
|
|
115
130
|
"""Build the Cedar authorizer with current entities."""
|
|
@@ -128,8 +143,9 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
128
143
|
if self._agent:
|
|
129
144
|
agent_uid = EntityUid(f"{self._namespace}::Agent", self._agent.id)
|
|
130
145
|
|
|
131
|
-
# Add tool entities from agent's tools
|
|
146
|
+
# Add tool entities from agent's tools and pre-parse response schemas
|
|
132
147
|
tool_entities: list[EntityUid] = []
|
|
148
|
+
self._tool_response_schemas = {}
|
|
133
149
|
for tool in self._agent.tools:
|
|
134
150
|
tool_id = tool.id or tool.name
|
|
135
151
|
tool_uid = EntityUid(f"{self._namespace}::Tool", tool_id)
|
|
@@ -142,6 +158,11 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
142
158
|
)
|
|
143
159
|
tool_entities.append(tool_uid)
|
|
144
160
|
entities.append(tool_entity)
|
|
161
|
+
# Pre-parse response JSON schema for use in _tool_response
|
|
162
|
+
if tool.response_json_schema:
|
|
163
|
+
self._tool_response_schemas[tool.name] = json.loads(
|
|
164
|
+
tool.response_json_schema
|
|
165
|
+
)
|
|
145
166
|
|
|
146
167
|
agent_entity = Entity(
|
|
147
168
|
agent_uid,
|
|
@@ -252,11 +273,47 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
252
273
|
assert request is not None, "Unexpected none request"
|
|
253
274
|
response = self._authorizer.is_authorized(request, self._policy_set)
|
|
254
275
|
if str(response.decision) == "Allow":
|
|
255
|
-
|
|
256
|
-
|
|
276
|
+
return Adjudication(
|
|
277
|
+
decision=Decision.ALLOW,
|
|
278
|
+
reason=f"Allowed by policies: {response.reason}",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
annotations: list[PolicyAnnotation] = []
|
|
282
|
+
hard_deny_ids = []
|
|
283
|
+
for internal_id in response.reason:
|
|
284
|
+
policy = self._policy_set.policy(internal_id)
|
|
285
|
+
if policy is None:
|
|
286
|
+
raise RuntimeError(f"Policy '{internal_id}' not found in policy set")
|
|
287
|
+
policy_annotations = policy.annotations()
|
|
288
|
+
if "escalate" not in policy_annotations:
|
|
289
|
+
hard_deny_ids.append(internal_id)
|
|
290
|
+
else:
|
|
291
|
+
custom = {
|
|
292
|
+
k: v
|
|
293
|
+
for k, v in policy_annotations.items()
|
|
294
|
+
if k not in ("id", "reason", "escalate")
|
|
295
|
+
}
|
|
296
|
+
annotations.append(
|
|
297
|
+
PolicyAnnotation(
|
|
298
|
+
id=policy_annotations.get("id", internal_id),
|
|
299
|
+
description=policy_annotations.get("reason", ""),
|
|
300
|
+
escalate=True,
|
|
301
|
+
escalate_arg=policy_annotations["escalate"],
|
|
302
|
+
custom=custom,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if not hard_deny_ids and annotations:
|
|
307
|
+
return Adjudication(
|
|
308
|
+
decision=Decision.ESCALATE,
|
|
309
|
+
reason=f"Escalated by policies: {response.reason}",
|
|
310
|
+
annotations=annotations,
|
|
311
|
+
)
|
|
257
312
|
else:
|
|
258
|
-
|
|
259
|
-
|
|
313
|
+
return Adjudication(
|
|
314
|
+
decision=Decision.DENY,
|
|
315
|
+
reason=f"Denied by policies: {hard_deny_ids}",
|
|
316
|
+
)
|
|
260
317
|
|
|
261
318
|
def _message_request(
|
|
262
319
|
self,
|
|
@@ -311,11 +368,17 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
311
368
|
action_name = tool_id.replace(" ", "_").replace("-", "_")
|
|
312
369
|
action_uid = EntityUid(f"{self._namespace}::Action", action_name)
|
|
313
370
|
|
|
314
|
-
context
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
371
|
+
# Build context - only include typed parameters if schema defines them
|
|
372
|
+
context_data: dict[str, object] = {
|
|
373
|
+
"parameters_json": json.dumps(content.args),
|
|
374
|
+
}
|
|
375
|
+
# Check if tool has typed parameters schema
|
|
376
|
+
if self._agent:
|
|
377
|
+
tool = next((t for t in self._agent.tools if t.name == tool_id), None)
|
|
378
|
+
if tool and tool.parameters_json_schema:
|
|
379
|
+
context_data["parameters"] = content.args
|
|
380
|
+
|
|
381
|
+
context = Context(context_data, schema=self._schema, action=action_uid)
|
|
319
382
|
|
|
320
383
|
return Request(
|
|
321
384
|
principal=agent_uid,
|
|
@@ -345,14 +408,22 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
345
408
|
action_name = tool_id.replace(" ", "_").replace("-", "_")
|
|
346
409
|
action_uid = EntityUid(f"{self._namespace}::Action", action_name)
|
|
347
410
|
|
|
348
|
-
context
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
411
|
+
# Build context - only include typed response if schema defines it
|
|
412
|
+
context_data: dict[str, object] = {
|
|
413
|
+
"response_json": json.dumps(content.response, default=str),
|
|
414
|
+
}
|
|
415
|
+
# Check if tool has typed response schema (pre-parsed in _build_authorizer)
|
|
416
|
+
if tool_id in self._tool_response_schemas:
|
|
417
|
+
response_schema = self._tool_response_schemas[tool_id]
|
|
418
|
+
# Check if the response schema is a simple type (not object/Record)
|
|
419
|
+
# Simple types get wrapped in {"value": ...} by the schema generator
|
|
420
|
+
if response_schema.get("type") not in ["object", "OBJECT"]:
|
|
421
|
+
# Simple type was wrapped in {"value": ...} by schema generator
|
|
422
|
+
context_data["response"] = {"value": content.response}
|
|
423
|
+
else:
|
|
424
|
+
context_data["response"] = content.response
|
|
425
|
+
|
|
426
|
+
context = Context(context_data, schema=self._schema, action=action_uid)
|
|
356
427
|
|
|
357
428
|
return Request(
|
|
358
429
|
principal=agent_uid,
|
sondera/langgraph/middleware.py
CHANGED
|
@@ -28,6 +28,7 @@ except ImportError:
|
|
|
28
28
|
|
|
29
29
|
from sondera.harness import Harness
|
|
30
30
|
from sondera.types import (
|
|
31
|
+
Decision,
|
|
31
32
|
PromptContent,
|
|
32
33
|
Role,
|
|
33
34
|
Stage,
|
|
@@ -173,7 +174,7 @@ class SonderaHarnessMiddleware(AgentMiddleware[State]):
|
|
|
173
174
|
f"[SonderaHarness] Before Agent Adjudication for trajectory {self._harness.trajectory_id}"
|
|
174
175
|
)
|
|
175
176
|
|
|
176
|
-
if adjudication.
|
|
177
|
+
if adjudication.decision == Decision.DENY:
|
|
177
178
|
self._log.warning(
|
|
178
179
|
f"[SonderaHarness] Policy violation detected (strategy={self._strategy.value}): "
|
|
179
180
|
f"{adjudication.reason}"
|
|
@@ -226,7 +227,7 @@ class SonderaHarnessMiddleware(AgentMiddleware[State]):
|
|
|
226
227
|
PromptContent(text=_message_to_text(request.messages[-1])),
|
|
227
228
|
)
|
|
228
229
|
|
|
229
|
-
if pre_adjudication.
|
|
230
|
+
if pre_adjudication.decision == Decision.DENY:
|
|
230
231
|
_LOGGER.warning(
|
|
231
232
|
f"[SonderaHarness] Pre-model policy violation (strategy={self._strategy.value}): "
|
|
232
233
|
f"{pre_adjudication.reason}"
|
|
@@ -259,7 +260,7 @@ class SonderaHarnessMiddleware(AgentMiddleware[State]):
|
|
|
259
260
|
self._log.info(
|
|
260
261
|
f"[SonderaHarness] Post-model Adjudication for trajectory {self._harness.trajectory_id}"
|
|
261
262
|
)
|
|
262
|
-
if post_adjudication.
|
|
263
|
+
if post_adjudication.decision == Decision.DENY:
|
|
263
264
|
self._log.warning(
|
|
264
265
|
f"[SonderaHarness] Post-model policy violation (strategy={self._strategy.value}): "
|
|
265
266
|
f"{post_adjudication.reason}"
|
|
@@ -324,7 +325,7 @@ class SonderaHarnessMiddleware(AgentMiddleware[State]):
|
|
|
324
325
|
f"[SonderaHarness] Before Tool Adjudication for trajectory {self._harness.trajectory_id}"
|
|
325
326
|
)
|
|
326
327
|
|
|
327
|
-
if pre_adjudication.
|
|
328
|
+
if pre_adjudication.decision == Decision.DENY:
|
|
328
329
|
self._log.warning(
|
|
329
330
|
f"[SonderaHarness] Pre-tool policy violation for {tool_name} "
|
|
330
331
|
f"(strategy={self._strategy.value}): {pre_adjudication.reason}"
|
|
@@ -367,7 +368,7 @@ class SonderaHarnessMiddleware(AgentMiddleware[State]):
|
|
|
367
368
|
f"[SonderaHarness] After Tool Adjudication for trajectory {self._harness.trajectory_id}"
|
|
368
369
|
)
|
|
369
370
|
|
|
370
|
-
if post_adjudication.
|
|
371
|
+
if post_adjudication.decision == Decision.DENY:
|
|
371
372
|
self._log.warning(
|
|
372
373
|
f"[SonderaHarness] Post-tool policy violation for {tool_name} "
|
|
373
374
|
f"(strategy={self._strategy.value}): {post_adjudication.reason}"
|
sondera/strands/harness.py
CHANGED
|
@@ -16,6 +16,7 @@ from strands.hooks.events import (
|
|
|
16
16
|
from sondera.harness import Harness
|
|
17
17
|
from sondera.strands.analyze import format_strands_agent
|
|
18
18
|
from sondera.types import (
|
|
19
|
+
Decision,
|
|
19
20
|
PromptContent,
|
|
20
21
|
Role,
|
|
21
22
|
Stage,
|
|
@@ -163,7 +164,7 @@ class SonderaHarnessHook(HookProvider):
|
|
|
163
164
|
f"[SonderaHarness] Before model adjudication for trajectory {self._harness.trajectory_id}"
|
|
164
165
|
)
|
|
165
166
|
|
|
166
|
-
if adjudication.
|
|
167
|
+
if adjudication.decision == Decision.DENY:
|
|
167
168
|
self._log.warning(
|
|
168
169
|
f"[SonderaHarness] Model call blocked: {adjudication.reason}"
|
|
169
170
|
)
|
|
@@ -192,7 +193,7 @@ class SonderaHarnessHook(HookProvider):
|
|
|
192
193
|
f"[SonderaHarness] After model adjudication for trajectory {self._harness.trajectory_id}"
|
|
193
194
|
)
|
|
194
195
|
|
|
195
|
-
if adjudication.
|
|
196
|
+
if adjudication.decision == Decision.DENY:
|
|
196
197
|
self._log.warning(
|
|
197
198
|
f"[SonderaHarness] Model response blocked: {adjudication.reason}"
|
|
198
199
|
)
|
|
@@ -231,7 +232,7 @@ class SonderaHarnessHook(HookProvider):
|
|
|
231
232
|
f"[SonderaHarness] Before tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
232
233
|
)
|
|
233
234
|
|
|
234
|
-
if adjudication.
|
|
235
|
+
if adjudication.decision == Decision.DENY:
|
|
235
236
|
# Cancel the tool call using Strands' cancel_tool mechanism
|
|
236
237
|
event.cancel_tool = f"Tool blocked by policy: {adjudication.reason}"
|
|
237
238
|
self._log.warning(
|
|
@@ -262,7 +263,7 @@ class SonderaHarnessHook(HookProvider):
|
|
|
262
263
|
f"[SonderaHarness] After tool adjudication for trajectory {self._harness.trajectory_id}"
|
|
263
264
|
)
|
|
264
265
|
|
|
265
|
-
if adjudication.
|
|
266
|
+
if adjudication.decision == Decision.DENY:
|
|
266
267
|
# Modify the result to indicate policy violation
|
|
267
268
|
event.result = {
|
|
268
269
|
"content": [
|
sondera/types.py
CHANGED
|
@@ -238,6 +238,10 @@ class PolicyAnnotation(Model):
|
|
|
238
238
|
"""Unique identifier of the policy that produced this annotation."""
|
|
239
239
|
description: str
|
|
240
240
|
"""Human-readable description of why this annotation was added."""
|
|
241
|
+
escalate: bool = False
|
|
242
|
+
"""Whether this policy requires escalation to a human or other oracle to decide the final verdict."""
|
|
243
|
+
escalate_arg: str = ""
|
|
244
|
+
"""The argument passed to @escalate, if any."""
|
|
241
245
|
custom: dict[str, str] = Field(default_factory=dict)
|
|
242
246
|
"""Custom key-value metadata from the policy."""
|
|
243
247
|
|
|
@@ -254,21 +258,6 @@ class Adjudication(Model):
|
|
|
254
258
|
annotations: list[PolicyAnnotation] = Field(default_factory=list)
|
|
255
259
|
"""Annotations from policy evaluations."""
|
|
256
260
|
|
|
257
|
-
@property
|
|
258
|
-
def is_denied(self) -> bool:
|
|
259
|
-
"""Check if is denied."""
|
|
260
|
-
return self.decision == Decision.DENY
|
|
261
|
-
|
|
262
|
-
@property
|
|
263
|
-
def is_allowed(self) -> bool:
|
|
264
|
-
"""Check if allowed."""
|
|
265
|
-
return self.decision == Decision.ALLOW
|
|
266
|
-
|
|
267
|
-
@property
|
|
268
|
-
def is_escalated(self) -> bool:
|
|
269
|
-
"""Check if result requires escalation."""
|
|
270
|
-
return self.decision == Decision.ESCALATE
|
|
271
|
-
|
|
272
261
|
|
|
273
262
|
class AdjudicatedStep(Model):
|
|
274
263
|
"""Result of the adjudicated input."""
|
|
@@ -282,27 +271,6 @@ class AdjudicatedStep(Model):
|
|
|
282
271
|
guardrails: GuardrailContext | None = None
|
|
283
272
|
"""Guardrail check results for this step."""
|
|
284
273
|
|
|
285
|
-
@property
|
|
286
|
-
def is_denied(self) -> bool:
|
|
287
|
-
"""Check if result is denied."""
|
|
288
|
-
return (
|
|
289
|
-
self.adjudication.decision == Decision.DENY
|
|
290
|
-
and self.mode == PolicyEngineMode.GOVERN
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
@property
|
|
294
|
-
def is_allowed(self) -> bool:
|
|
295
|
-
"""Check if result is allowed."""
|
|
296
|
-
return self.adjudication.decision == Decision.ALLOW
|
|
297
|
-
|
|
298
|
-
@property
|
|
299
|
-
def is_escalated(self) -> bool:
|
|
300
|
-
"""Check if result requires escalation."""
|
|
301
|
-
return (
|
|
302
|
-
self.adjudication.decision == Decision.ESCALATE
|
|
303
|
-
and self.mode == PolicyEngineMode.GOVERN
|
|
304
|
-
)
|
|
305
|
-
|
|
306
274
|
@property
|
|
307
275
|
def message(self) -> str:
|
|
308
276
|
"""Get the adjudication reason in a friendly format."""
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sondera-harness
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Sondera Harness SDK for Python - Agent governance and policy enforcement
|
|
5
|
+
Author-email: Sondera AI <sdk@sondera.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sondera-ai/harness-sdk-python
|
|
8
|
+
Project-URL: Documentation, https://docs.sondera.ai
|
|
9
|
+
Project-URL: Repository, https://github.com/sondera-ai/harness-sdk-python
|
|
10
|
+
Project-URL: Issues, https://github.com/sondera-ai/harness-sdk-python/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/sondera-ai/harness-sdk-python/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: ai,agents,governance,policy,guardrails,llm,langchain,langgraph
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: <3.15,>=3.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: cedar-python>=0.1.1
|
|
27
|
+
Requires-Dist: click>=8.0.0
|
|
28
|
+
Requires-Dist: click-default-group>=1.2.4
|
|
29
|
+
Requires-Dist: grpcio>=1.76.0
|
|
30
|
+
Requires-Dist: grpcio-tools>=1.76.0
|
|
31
|
+
Requires-Dist: httpx>=0.27.0
|
|
32
|
+
Requires-Dist: pydantic>=2.12.0
|
|
33
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
34
|
+
Requires-Dist: textual>=6.11.0
|
|
35
|
+
Provides-Extra: adk
|
|
36
|
+
Requires-Dist: google-adk>=1.22.0; extra == "adk"
|
|
37
|
+
Provides-Extra: langgraph
|
|
38
|
+
Requires-Dist: langchain>=1.2.0; extra == "langgraph"
|
|
39
|
+
Requires-Dist: langgraph>=1.0.5; extra == "langgraph"
|
|
40
|
+
Provides-Extra: strands
|
|
41
|
+
Requires-Dist: strands-agents>=1.21.0; extra == "strands"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: google-adk>=1.22.0; extra == "all"
|
|
44
|
+
Requires-Dist: langchain>=1.2.0; extra == "all"
|
|
45
|
+
Requires-Dist: langgraph>=1.0.5; extra == "all"
|
|
46
|
+
Requires-Dist: strands-agents>=1.21.0; extra == "all"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
<div align="center">
|
|
50
|
+
|
|
51
|
+
<h1>Sondera Harness</h1>
|
|
52
|
+
|
|
53
|
+
<p><strong>Deterministic guardrails for AI agents.</strong></p>
|
|
54
|
+
|
|
55
|
+
<p>Open-source. Works with LangGraph, ADK, Strands, or any custom agent.</p>
|
|
56
|
+
|
|
57
|
+
<p>
|
|
58
|
+
<a href="https://docs.sondera.ai/">Docs</a>
|
|
59
|
+
·
|
|
60
|
+
<a href="https://docs.sondera.ai/quickstart/">Quickstart</a>
|
|
61
|
+
·
|
|
62
|
+
<a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
|
|
63
|
+
·
|
|
64
|
+
<a href="https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw">Slack</a>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<p>
|
|
68
|
+
<a href="https://pypi.org/project/sondera-harness/"><img src="https://img.shields.io/pypi/v/sondera-harness.svg" alt="PyPI version"></a>
|
|
69
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
|
|
70
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/sondera-ai/sondera-harness-python.svg" alt="License: MIT"></a>
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## What is Sondera Harness?
|
|
78
|
+
|
|
79
|
+
Sondera Harness evaluates [Cedar](https://www.cedarpolicy.com/) policies before your agent's actions execute. When a policy denies an action, the agent gets a reason why and can try a different approach. Same input, same verdict. Deterministic, not probabilistic.
|
|
80
|
+
|
|
81
|
+
**Example policy:**
|
|
82
|
+
|
|
83
|
+
```cedar
|
|
84
|
+
forbid(principal, action, resource)
|
|
85
|
+
when { context has parameters_json && context.parameters_json like "*rm -rf*" };
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This policy stops your agent from running `rm -rf`, every time.
|
|
89
|
+
|
|
90
|
+
## Quickstart
|
|
91
|
+
|
|
92
|
+
> **Try it now:** [](https://colab.research.google.com/github/sondera-ai/sondera-harness-python/blob/main/docs/src/notebooks/quickstart.ipynb) - no install required.
|
|
93
|
+
|
|
94
|
+
### 1. Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Works with [LangChain/LangGraph](https://docs.sondera.ai/integrations/langgraph/), [Google ADK](https://docs.sondera.ai/integrations/adk/), [Strands](https://docs.sondera.ai/integrations/strands/), and [custom agents](https://docs.sondera.ai/integrations/custom/).
|
|
101
|
+
|
|
102
|
+
### 2. Add to Your Agent (LangGraph)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from langchain.agents import create_agent
|
|
106
|
+
from sondera.harness import SonderaRemoteHarness
|
|
107
|
+
from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
|
|
108
|
+
|
|
109
|
+
# Analyze your tools and create agent metadata
|
|
110
|
+
sondera_agent = create_agent_from_langchain_tools(
|
|
111
|
+
tools=my_tools,
|
|
112
|
+
agent_id="langchain-agent",
|
|
113
|
+
agent_name="My LangChain Agent",
|
|
114
|
+
agent_description="An agent that helps with tasks",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create harness with agent
|
|
118
|
+
harness = SonderaRemoteHarness(agent=sondera_agent)
|
|
119
|
+
|
|
120
|
+
# Create middleware
|
|
121
|
+
middleware = SonderaHarnessMiddleware(
|
|
122
|
+
harness=harness,
|
|
123
|
+
strategy=Strategy.BLOCK, # or Strategy.STEER
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Create agent with middleware
|
|
127
|
+
agent = create_agent(
|
|
128
|
+
model=my_model,
|
|
129
|
+
tools=my_tools,
|
|
130
|
+
middleware=[middleware],
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> [!NOTE]
|
|
135
|
+
> This example uses Sondera Platform ([free account](https://sondera.ai)), which also enables the TUI below. For local-only development, see [CedarPolicyHarness](https://docs.sondera.ai/integrations/custom/).
|
|
136
|
+
|
|
137
|
+
### 3. See It in Action
|
|
138
|
+
|
|
139
|
+
<div align="center">
|
|
140
|
+
<img src="docs/src/assets/sondera-tui.gif" alt="Sondera TUI" width="700" />
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run sondera # or: sondera (if installed via pip)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Why Sondera Harness?
|
|
148
|
+
|
|
149
|
+
- **Steer, don't block:** Denied actions include a reason. Return it to the model, and it tries something else.
|
|
150
|
+
- **Deterministic:** Stop debugging prompts. Rules are predictable.
|
|
151
|
+
- **Drop-in integration:** Native middleware for LangGraph, Google ADK, and Strands.
|
|
152
|
+
- **Full observability:** Every action, every decision, every reason. Audit-ready.
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [Quickstart](https://docs.sondera.ai/quickstart/)
|
|
157
|
+
- [Writing Policies](https://docs.sondera.ai/writing-policies/)
|
|
158
|
+
- [Integrations](https://docs.sondera.ai/integrations/)
|
|
159
|
+
- [Reference](https://docs.sondera.ai/reference/)
|
|
160
|
+
|
|
161
|
+
## Community
|
|
162
|
+
|
|
163
|
+
- [Slack](https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw) for questions and feedback
|
|
164
|
+
- [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
|
|
165
|
+
- [Contributing](CONTRIBUTING.md) for development setup
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
[MIT](LICENSE)
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
sondera/__init__.py,sha256=
|
|
1
|
+
sondera/__init__.py,sha256=0Z1R6ZkA5VSE_3ZCCgfeecm5nU6mFBdl4-18b8Nwt3Y,3558
|
|
2
2
|
sondera/__main__.py,sha256=MNgWvrV4g4Ot653Ngi2D4cAOyRIG19qHAWLzQecsMdg,66
|
|
3
3
|
sondera/cli.py,sha256=owchF-eA6kttYGTSsLl0B7XJMmn9O2n2LFjpfYQvznQ,494
|
|
4
4
|
sondera/exceptions.py,sha256=vtuToFc5tSlzAyVYvayyOatKBcoenisDA1RN2yk9aSI,3584
|
|
5
5
|
sondera/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
sondera/settings.py,sha256=bLB98vT75aXKh5ihYnCd0dTk1AdfUUaGuPkyzhcldE0,459
|
|
7
|
-
sondera/types.py,sha256=
|
|
7
|
+
sondera/types.py,sha256=4dnAqwUCYPsS6Tm6QchRsIQmuAyTcGnDnV2txewtg2I,9402
|
|
8
8
|
sondera/adk/__init__.py,sha256=weoilnJyr8JNBv2HK6s3hhW-6rOBGBcNwwkKk1oHVFE,77
|
|
9
9
|
sondera/adk/analyze.py,sha256=IurwCWPZlNbMkIwi3TGWUu4k-w_VmKCkAnVFbfipbxY,7974
|
|
10
|
-
sondera/adk/plugin.py,sha256=
|
|
10
|
+
sondera/adk/plugin.py,sha256=U6bhPCawBOJBE802ECFEyVVUNn5qouMHnM-awDrfkLQ,13141
|
|
11
11
|
sondera/harness/__init__.py,sha256=gK0rFEyixD9X67pFyWKQpcq_oZ6pep6up8K0Y-zvXUc,221
|
|
12
12
|
sondera/harness/abc.py,sha256=hL77Rlzy1B-DjFexhskGm_9j5Sue-TaWdlSnjr-Al70,3548
|
|
13
13
|
sondera/harness/cedar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
sondera/harness/cedar/harness.py,sha256=
|
|
14
|
+
sondera/harness/cedar/harness.py,sha256=ZTUa1TuzNBLESkLMZZu_jAVOnqMOYLGgD1RbylqbhMk,15597
|
|
15
15
|
sondera/harness/cedar/schema.py,sha256=jDAGdLciK3fQ-7yKrqeFkU4YHQg-KwWvTi1leEuYVxo,7766
|
|
16
16
|
sondera/harness/sondera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
sondera/harness/sondera/_grpc.py,sha256=vLVJwJBtDwX-p8qZPfShpChKxdP3lSFoTV6QiL4dh7s,14708
|
|
@@ -20,7 +20,7 @@ sondera/langgraph/__init__.py,sha256=F2eNoPp944tvGbf9a4etNh3-o49WOPrcs9EFVT0OYYw
|
|
|
20
20
|
sondera/langgraph/analyze.py,sha256=1n-1yKr7-kfdFNSBe9JozZ08oJeYvPqKkFttcQ4MXHI,20514
|
|
21
21
|
sondera/langgraph/exceptions.py,sha256=BRdh1gpELb3_WZ9Bh_UUwZsIOcIPrpQCD-7LnnAGeV4,501
|
|
22
22
|
sondera/langgraph/graph.py,sha256=hIF5q_Fbq4E16CDqDksLrCETTEORgKiqPlpqIB5F-Rc,6898
|
|
23
|
-
sondera/langgraph/middleware.py,sha256=
|
|
23
|
+
sondera/langgraph/middleware.py,sha256=J3bUmW1Aor6q2DImqPSmrmX2yxDKfcFs9v35XEi66Cs,16910
|
|
24
24
|
sondera/proto/google/protobuf/any_pb2.py,sha256=W6duyvBgx7RvePFCrJSxWagU7ddj1W9l8CsjarJJPOs,1703
|
|
25
25
|
sondera/proto/google/protobuf/any_pb2.pyi,sha256=SSPWnvAxd1bX9FYZmLrZVmQ-29GsAE5bmgBcJlMaVfw,587
|
|
26
26
|
sondera/proto/google/protobuf/any_pb2_grpc.py,sha256=OVxvViTmAZH370lIhyTfRI29xiCur_xZyAI4VJY1qJg,899
|
|
@@ -52,7 +52,7 @@ sondera/proto/sondera/harness/v1/harness_pb2.pyi,sha256=NiQNpGD9ICD41I9w11yJJwec
|
|
|
52
52
|
sondera/proto/sondera/harness/v1/harness_pb2_grpc.py,sha256=h5y_HwqqzzpjaqQuaUt5ICy-2B2UJ-jea0gYfXsB6Ig,21845
|
|
53
53
|
sondera/strands/__init__.py,sha256=Tg0l3ERb_uusENXZv9mtZz5tJ-TLK7K8zG2KsKHmUn4,124
|
|
54
54
|
sondera/strands/analyze.py,sha256=yT9_DGieoMIxy5DGma9EdeAl2FVnkFQkdqK8waTphB8,8007
|
|
55
|
-
sondera/strands/harness.py,sha256=
|
|
55
|
+
sondera/strands/harness.py,sha256=helyy9AaaO7dVas2UcC0SfTWiylsaNXl_j7PZx0LVmY,13178
|
|
56
56
|
sondera/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
57
|
sondera/tui/app.py,sha256=I6Pcyatr5pPqjYwmBZrdTSFY11oR9sWmG90gVbN9pqo,11521
|
|
58
58
|
sondera/tui/app.tcss,sha256=AE239R11BCuhKj8ZxvMYXduZw_-H11I7UkTDh72qdME,6562
|
|
@@ -70,9 +70,9 @@ sondera/tui/widgets/tool_card.py,sha256=3yNQcc_umcan4V1S5GiEKd7l4YA_atibwn3HF0n6
|
|
|
70
70
|
sondera/tui/widgets/violation_panel.py,sha256=fowe4KWb13NXLX0_RAxEPdRqYeyGzlImpRs4_L9y1zI,2933
|
|
71
71
|
sondera/tui/widgets/violations_list.py,sha256=86qICAsQOC6kjQLs64WxK7u59vEJ8kvfiToLVlzFyHM,2866
|
|
72
72
|
sondera/tui/widgets/violations_summary.py,sha256=e2LwqlB1aS8sZ2gEC5clk7siA16NSgePU1mpv8T1iTc,4473
|
|
73
|
-
sondera_harness-0.
|
|
74
|
-
sondera_harness-0.
|
|
75
|
-
sondera_harness-0.
|
|
76
|
-
sondera_harness-0.
|
|
77
|
-
sondera_harness-0.
|
|
78
|
-
sondera_harness-0.
|
|
73
|
+
sondera_harness-0.7.0.dist-info/licenses/LICENSE,sha256=DmSfauhgrslTxZOcDAmcYqsqsKBkMqVh3PYdjPghNbU,1070
|
|
74
|
+
sondera_harness-0.7.0.dist-info/METADATA,sha256=mHC8kmhaMjapN72zLii3afE304_Bd-_LLHvwZEl77K0,6361
|
|
75
|
+
sondera_harness-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
76
|
+
sondera_harness-0.7.0.dist-info/entry_points.txt,sha256=5cLgW0-GzEwNnQjXIhGT21iFprQb1lftBaFJjC4IrgE,78
|
|
77
|
+
sondera_harness-0.7.0.dist-info/top_level.txt,sha256=BR0X8Gq9CCpwbQg5evpQfy5zwp9fTuGnlJhXSNqQ_hA,8
|
|
78
|
+
sondera_harness-0.7.0.dist-info/RECORD,,
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sondera-harness
|
|
3
|
-
Version: 0.6.2
|
|
4
|
-
Summary: Sondera Harness SDK for Python - Agent governance and policy enforcement
|
|
5
|
-
Author-email: Sondera AI <sdk@sondera.ai>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/sondera-ai/harness-sdk-python
|
|
8
|
-
Project-URL: Documentation, https://docs.sondera.ai
|
|
9
|
-
Project-URL: Repository, https://github.com/sondera-ai/harness-sdk-python
|
|
10
|
-
Project-URL: Issues, https://github.com/sondera-ai/harness-sdk-python/issues
|
|
11
|
-
Project-URL: Changelog, https://github.com/sondera-ai/harness-sdk-python/blob/main/CHANGELOG.md
|
|
12
|
-
Keywords: ai,agents,governance,policy,guardrails,llm,langchain,langgraph
|
|
13
|
-
Classifier: Development Status :: 4 - Beta
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
-
Classifier: Topic :: Security
|
|
22
|
-
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: <3.15,>=3.12
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
Requires-Dist: cedar-python>=0.1.1
|
|
27
|
-
Requires-Dist: click>=8.0.0
|
|
28
|
-
Requires-Dist: click-default-group>=1.2.4
|
|
29
|
-
Requires-Dist: grpcio>=1.76.0
|
|
30
|
-
Requires-Dist: grpcio-tools>=1.76.0
|
|
31
|
-
Requires-Dist: httpx>=0.27.0
|
|
32
|
-
Requires-Dist: pydantic>=2.12.0
|
|
33
|
-
Requires-Dist: pydantic-settings>=2.12.0
|
|
34
|
-
Requires-Dist: textual>=6.11.0
|
|
35
|
-
Provides-Extra: adk
|
|
36
|
-
Requires-Dist: google-adk>=1.22.0; extra == "adk"
|
|
37
|
-
Provides-Extra: langgraph
|
|
38
|
-
Requires-Dist: langchain>=1.2.0; extra == "langgraph"
|
|
39
|
-
Requires-Dist: langgraph>=1.0.5; extra == "langgraph"
|
|
40
|
-
Provides-Extra: strands
|
|
41
|
-
Requires-Dist: strands-agents>=1.21.0; extra == "strands"
|
|
42
|
-
Provides-Extra: all
|
|
43
|
-
Requires-Dist: google-adk>=1.22.0; extra == "all"
|
|
44
|
-
Requires-Dist: langchain>=1.2.0; extra == "all"
|
|
45
|
-
Requires-Dist: langgraph>=1.0.5; extra == "all"
|
|
46
|
-
Requires-Dist: strands-agents>=1.21.0; extra == "all"
|
|
47
|
-
Dynamic: license-file
|
|
48
|
-
|
|
49
|
-
# Sondera Harness SDK for Python
|
|
50
|
-
|
|
51
|
-
[](https://www.python.org/downloads/)
|
|
52
|
-
[](LICENSE)
|
|
53
|
-
|
|
54
|
-
>
|
|
55
|
-
> One step at a time. One action at a time. One trajectory at a time.
|
|
56
|
-
>
|
|
57
|
-
|
|
58
|
-
AI agents systems operate beyond traditional security boundaries, making autonomous decisions, calling tools, and accessing resources based on context that changes with every execution. Sondera SDK provides runtime governance for these agentic systems, answering not just "can this agent do X?" but "should it do X here, now, with this data?" Built for developers deploying agents through LangGraph, Google ADK, and Strands, Sondera enables real-time trajectory tracking, policy-as-code enforcement via Cedar, and behavioral adjudication so you can ship agents with confidence.
|
|
59
|
-
|
|
60
|
-
## Features
|
|
61
|
-
|
|
62
|
-
- **Managed harness-as-a-service** with the Sondera Harness for enterprise policy governance and guardrails
|
|
63
|
-
- **Local policy-as-code** using Cedar policy language in the Cedar Policy Harness
|
|
64
|
-
- **Real-time trajectory** observability, adjudication, and steering
|
|
65
|
-
- **Scaffold integrations** for LangGraph, Google ADK, and Strands
|
|
66
|
-
- **CLI and TUI** for monitoring agent behavior
|
|
67
|
-
|
|
68
|
-
## Installation
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
uv add sondera-harness
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Optional Dependencies
|
|
75
|
-
|
|
76
|
-
Install extras for specific framework integrations:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
# Google ADK support
|
|
80
|
-
uv add sondera-harness --extra adk
|
|
81
|
-
|
|
82
|
-
# LangGraph support
|
|
83
|
-
uv add sondera-harness --extra langgraph
|
|
84
|
-
|
|
85
|
-
# Strands support
|
|
86
|
-
uv add sondera-harness --extra strands
|
|
87
|
-
|
|
88
|
-
# All integrations
|
|
89
|
-
uv add sondera-harness --all-extras
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Quick Start
|
|
93
|
-
|
|
94
|
-
### Configuration
|
|
95
|
-
|
|
96
|
-
Set your API credentials via environment variables:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
export SONDERA_HARNESS_ENDPOINT="your-harness.sondera.ai:443"
|
|
100
|
-
export SONDERA_API_TOKEN="<YOUR_SONDERA_API_KEY>"
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Or create a `.env` file or `~/.sondera/env`:
|
|
104
|
-
|
|
105
|
-
```env
|
|
106
|
-
SONDERA_HARNESS_ENDPOINT=your-harness.sondera.ai:443
|
|
107
|
-
SONDERA_API_TOKEN=<YOUR_SONDERA_API_KEY>
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Scaffold Integrations
|
|
111
|
-
|
|
112
|
-
### LangGraph / LangChain
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
from langchain.agents import create_agent
|
|
116
|
-
from sondera.harness import SonderaRemoteHarness
|
|
117
|
-
from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
|
|
118
|
-
|
|
119
|
-
# Analyze your tools and create agent metadata
|
|
120
|
-
sondera_agent = create_agent_from_langchain_tools(
|
|
121
|
-
tools=my_tools,
|
|
122
|
-
agent_id="langchain-agent",
|
|
123
|
-
agent_name="My LangChain Agent",
|
|
124
|
-
agent_description="An agent that helps with tasks",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Create harness with agent
|
|
128
|
-
harness = SonderaRemoteHarness(agent=sondera_agent)
|
|
129
|
-
|
|
130
|
-
# Create middleware
|
|
131
|
-
middleware = SonderaHarnessMiddleware(
|
|
132
|
-
harness=harness,
|
|
133
|
-
strategy=Strategy.BLOCK, # or Strategy.STEER
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# Create agent with middleware
|
|
137
|
-
agent = create_agent(
|
|
138
|
-
model=my_model,
|
|
139
|
-
tools=my_tools,
|
|
140
|
-
middleware=[middleware],
|
|
141
|
-
)
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Google ADK
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
from google.adk.agents import Agent
|
|
148
|
-
from google.adk.runners import Runner
|
|
149
|
-
from sondera.harness import SonderaRemoteHarness
|
|
150
|
-
from sondera.adk import SonderaHarnessPlugin
|
|
151
|
-
|
|
152
|
-
# Create harness
|
|
153
|
-
harness = SonderaRemoteHarness(
|
|
154
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Create plugin
|
|
158
|
-
plugin = SonderaHarnessPlugin(harness=harness)
|
|
159
|
-
|
|
160
|
-
# Create agent
|
|
161
|
-
agent = Agent(
|
|
162
|
-
name="my-adk-agent",
|
|
163
|
-
model="gemini-2.0-flash",
|
|
164
|
-
instruction="Be helpful and safe",
|
|
165
|
-
tools=[...],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Create runner with plugin
|
|
169
|
-
runner = Runner(
|
|
170
|
-
agent=agent,
|
|
171
|
-
app_name="my-app",
|
|
172
|
-
plugins=[plugin],
|
|
173
|
-
)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Strands Agents
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
from strands import Agent
|
|
180
|
-
from sondera.harness import SonderaRemoteHarness
|
|
181
|
-
from sondera.strands import SonderaHarnessHook
|
|
182
|
-
|
|
183
|
-
# Create harness
|
|
184
|
-
harness = SonderaRemoteHarness(
|
|
185
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
# Create hook
|
|
189
|
-
hook = SonderaHarnessHook(harness=harness)
|
|
190
|
-
|
|
191
|
-
# Create agent with hook
|
|
192
|
-
agent = Agent(
|
|
193
|
-
system_prompt="You are a helpful assistant",
|
|
194
|
-
model="anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
195
|
-
hooks=[hook],
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
# Run agent (hooks fire automatically)
|
|
199
|
-
response = agent("What is 5 + 3?")
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Custom Scaffold
|
|
203
|
-
|
|
204
|
-
```python
|
|
205
|
-
from sondera import SonderaRemoteHarness, Agent, PromptContent, Role, Stage
|
|
206
|
-
|
|
207
|
-
# Create a harness instance
|
|
208
|
-
harness = SonderaRemoteHarness(
|
|
209
|
-
sondera_harness_endpoint="localhost:50051",
|
|
210
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
211
|
-
sondera_harness_client_secure=True, # Enable TLS for production
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
# Define your agent
|
|
215
|
-
agent = Agent(
|
|
216
|
-
id="my-agent",
|
|
217
|
-
provider_id="custom",
|
|
218
|
-
name="My Assistant",
|
|
219
|
-
description="A helpful AI assistant",
|
|
220
|
-
instruction="Be helpful, accurate, and safe",
|
|
221
|
-
tools=[],
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Initialize a trajectory
|
|
225
|
-
await harness.initialize(agent=agent)
|
|
226
|
-
|
|
227
|
-
# Adjudicate user input
|
|
228
|
-
adjudication = await harness.adjudicate(
|
|
229
|
-
Stage.PRE_MODEL,
|
|
230
|
-
Role.USER,
|
|
231
|
-
PromptContent(text="Hello, can you help me?"),
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
if adjudication.is_allowed:
|
|
235
|
-
# Proceed with agent logic
|
|
236
|
-
pass
|
|
237
|
-
elif adjudication.is_denied:
|
|
238
|
-
print(f"Request blocked: {adjudication.reason}")
|
|
239
|
-
|
|
240
|
-
# Finalize the trajectory
|
|
241
|
-
await harness.finalize()
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Remote and Local Harnesses
|
|
245
|
-
|
|
246
|
-
### Cedar Policy Harness (Local Only)
|
|
247
|
-
|
|
248
|
-
For a local harness deployment, you can use the `CedarPolicyHarness` to evaluate Cedar policies:
|
|
249
|
-
|
|
250
|
-
```python
|
|
251
|
-
from sondera.harness import CedarPolicyHarness
|
|
252
|
-
from sondera import Agent
|
|
253
|
-
|
|
254
|
-
# Define Cedar policies
|
|
255
|
-
policies = '''
|
|
256
|
-
@id("forbid-dangerous-bash")
|
|
257
|
-
forbid(
|
|
258
|
-
principal,
|
|
259
|
-
action == Coding_Agent::Action::"Bash",
|
|
260
|
-
resource
|
|
261
|
-
)
|
|
262
|
-
when {
|
|
263
|
-
context has parameters &&
|
|
264
|
-
(context.parameters.command like "*rm -rf /*" ||
|
|
265
|
-
context.parameters.command like "*mkfs*" ||
|
|
266
|
-
context.parameters.command like "*dd if=/dev/zero*" ||
|
|
267
|
-
context.parameters.command like "*> /dev/sda*")
|
|
268
|
-
};
|
|
269
|
-
'''
|
|
270
|
-
|
|
271
|
-
# Create local policy engine
|
|
272
|
-
harness = CedarPolicyHarness(
|
|
273
|
-
policy_set=policies,
|
|
274
|
-
agent=my_agent,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
await harness.initialize()
|
|
278
|
-
# Use same adjudication API as RemoteHarness
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## CLI & TUI
|
|
282
|
-
|
|
283
|
-
Launch the Sondera TUI for monitoring (note, requires a Sondera account and API key):
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
sondera
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The TUI provides:
|
|
290
|
-
- Real-time agent and trajectory overview
|
|
291
|
-
- Adjudication history and policy violations
|
|
292
|
-
- Agent details and tool inspection
|
|
293
|
-
|
|
294
|
-
## Examples
|
|
295
|
-
|
|
296
|
-
See the [examples/](examples/) directory for complete demos:
|
|
297
|
-
|
|
298
|
-
- **LangGraph**: Investment chatbot with policy enforcement
|
|
299
|
-
- **ADK**: Payment and healthcare agents
|
|
300
|
-
- **Strands**: Various agent implementations
|
|
301
|
-
|
|
302
|
-
## Environment Variables
|
|
303
|
-
|
|
304
|
-
| Variable | Description | Default |
|
|
305
|
-
|----------|-------------|---------|
|
|
306
|
-
| `SONDERA_HARNESS_ENDPOINT` | Harness service endpoint | `localhost:50051` |
|
|
307
|
-
| `SONDERA_API_TOKEN` | JWT authentication token | Required for remote |
|
|
308
|
-
|
|
309
|
-
## Requirements
|
|
310
|
-
|
|
311
|
-
- Python 3.12 or higher (up to 3.14)
|
|
312
|
-
|
|
313
|
-
## Security
|
|
314
|
-
|
|
315
|
-
See [SECURITY.md](SECURITY.md) for security best practices and vulnerability reporting.
|
|
316
|
-
|
|
317
|
-
## Contributing
|
|
318
|
-
|
|
319
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
320
|
-
|
|
321
|
-
## License
|
|
322
|
-
|
|
323
|
-
MIT - see [LICENSE](LICENSE) for details.
|
|
File without changes
|
|
File without changes
|