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 CHANGED
@@ -1,28 +1,44 @@
1
- """Sondera SDK for Python - Agent governance and policy enforcement.
1
+ """Sondera Harness - Steer agents with rules, not prompts.
2
2
 
3
- This SDK provides tools for integrating AI agents with the Sondera Platform
4
- for policy enforcement, guardrails, and governance.
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
- Main Components:
7
- - Harness: Abstract base class for harness implementations
8
- - RemoteHarness: Production harness connecting to Sondera Platform
9
- - CedarPolicyEngine: Local policy-as-code engine using Cedar
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/LangChain middleware
22
+ - sondera.langgraph: LangGraph middleware
13
23
  - sondera.adk: Google ADK plugin
14
- - sondera.strands: Strands Agent SDK hook
24
+ - sondera.strands: Strands lifecycle hooks
15
25
 
16
26
  Example:
17
- >>> from sondera import SonderaRemoteHarness, Agent
18
- >>> harness = SonderaRemoteHarness(sondera_api_key="<YOUR_SONDERA_API_KEY>")
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="langchain",
22
- ... name="My Agent",
32
+ ... provider_id="local",
33
+ ... name="My_Agent",
23
34
  ... description="A helpful assistant",
24
- ... instruction="Be helpful and concise",
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.0-flash", ...)
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.is_denied:
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.is_denied:
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.is_denied:
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.is_denied:
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.is_denied:
336
+ if adjudication.decision == Decision.DENY:
336
337
  return {"error": f"Tool result blocked: {adjudication.reason}"}
337
338
  return None
338
339
 
@@ -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 = 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
- reason = f"Allowed by policies: {response.reason}"
256
- return Adjudication(decision=Decision.ALLOW, reason=reason)
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
- reason = f"Denied by policies: {response.reason}"
259
- return Adjudication(decision=Decision.DENY, reason=reason)
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 = Context(
315
- {"parameters_json": json.dumps(content.args), "parameters": content.args},
316
- schema=self._schema,
317
- action=action_uid,
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 = Context(
349
- {
350
- "response_json": json.dumps(content.response, default=str),
351
- "response": content.response,
352
- },
353
- schema=self._schema,
354
- action=action_uid,
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,
@@ -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.is_denied:
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.is_denied:
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.is_denied:
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.is_denied:
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.is_denied:
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}"
@@ -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.is_denied:
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.is_denied:
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.is_denied:
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.is_denied:
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:** [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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=g6Ewr03q_2JZGIJ9APVs-6MLvZsAEtQJ2dcYEp4a6oc,2695
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=3rG4aeVM_Pfrv83aGSwnpKPwQmh72O6eqYijZnhown8,10216
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=HfVdbzp0bHc4YIs0MjNO-pwrMMex1AsKbQWoQNp7dmI,13047
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=mkskBjz4REVhV79r_zgrRBQjwGqbKom7NlpJr8aM0P8,12128
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=q29nATkKlBiPs0bWuDpABkWccgYIBB6voLUnbQi1x0c,16816
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=CazoBF-grM2D2Ipj68QdYW4BmWw9fdLVy3_ntcylXvs,13100
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.6.2.dist-info/licenses/LICENSE,sha256=DmSfauhgrslTxZOcDAmcYqsqsKBkMqVh3PYdjPghNbU,1070
74
- sondera_harness-0.6.2.dist-info/METADATA,sha256=rLrBiEELg0dIbXRdpJP_CFid6KBKIO79H-lrQXeY2K8,8899
75
- sondera_harness-0.6.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
76
- sondera_harness-0.6.2.dist-info/entry_points.txt,sha256=e9aHpIPUUlP5MPKORk7k6ekUfZLN3RyO1MEJa-nCzK4,44
77
- sondera_harness-0.6.2.dist-info/top_level.txt,sha256=BR0X8Gq9CCpwbQg5evpQfy5zwp9fTuGnlJhXSNqQ_hA,8
78
- sondera_harness-0.6.2.dist-info/RECORD,,
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  sondera = sondera.cli:cli
3
+ sondera-harness = sondera.cli:cli
@@ -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
- [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
52
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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.