sondera-harness 0.6.3__py3-none-any.whl → 0.7.1__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
@@ -68,8 +68,8 @@ from sondera.types import (
68
68
  Content,
69
69
  Decision,
70
70
  Parameter,
71
- PolicyAnnotation,
72
71
  PolicyEngineMode,
72
+ PolicyMetadata,
73
73
  PromptContent,
74
74
  Role,
75
75
  SourceCode,
@@ -112,7 +112,7 @@ __all__ = [
112
112
  "AdjudicatedStep",
113
113
  "AdjudicatedTrajectory",
114
114
  "AdjudicationRecord",
115
- "PolicyAnnotation",
115
+ "PolicyMetadata",
116
116
  "Decision",
117
117
  # Exceptions
118
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,
@@ -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
 
@@ -15,6 +15,7 @@ from cedar import (
15
15
  EntityUid,
16
16
  PolicySet,
17
17
  Request,
18
+ Response,
18
19
  Schema,
19
20
  )
20
21
  from sondera.harness.abc import Harness as AbstractHarness
@@ -23,7 +24,7 @@ from sondera.types import (
23
24
  Agent,
24
25
  Content,
25
26
  Decision,
26
- PolicyAnnotation,
27
+ PolicyMetadata,
27
28
  PromptContent,
28
29
  Role,
29
30
  Stage,
@@ -95,7 +96,6 @@ class CedarPolicyHarness(AbstractHarness):
95
96
  if policy_set is None:
96
97
  raise ValueError("policy_set is required")
97
98
 
98
- self._cedar_schema = schema
99
99
  # Exclude None values when serializing to JSON for Cedar compatibility
100
100
  self._schema = Schema.from_json(schema.model_dump_json(exclude_none=True))
101
101
 
@@ -105,10 +105,18 @@ class CedarPolicyHarness(AbstractHarness):
105
105
  else:
106
106
  self._policy_set = policy_set
107
107
 
108
+ seen_ids: set[str] = set()
108
109
  for policy in self._policy_set.policies():
109
110
  annotations = policy.annotations()
111
+ if "id" not in annotations:
112
+ raise ValueError(
113
+ f"Policy '{policy.id()}' is missing required @id annotation."
114
+ )
115
+ policy_id = annotations["id"]
116
+ if policy_id in seen_ids:
117
+ self._logger.warning(f"Duplicate policy @id: '{policy_id}'")
118
+ seen_ids.add(policy_id)
110
119
  if "escalate" in annotations and str(policy.effect()) != "Forbid":
111
- policy_id = annotations.get("id", policy.id())
112
120
  raise ValueError(
113
121
  f"Policy '{policy_id}' has @escalate but is not a forbid policy. "
114
122
  "@escalate is only valid on forbid policies."
@@ -270,50 +278,65 @@ class CedarPolicyHarness(AbstractHarness):
270
278
  decision=Decision.ALLOW,
271
279
  reason="Non-tool content allowed by default",
272
280
  )
273
- assert request is not None, "Unexpected none request"
274
281
  response = self._authorizer.is_authorized(request, self._policy_set)
275
- if str(response.decision) == "Allow":
276
- return Adjudication(
277
- decision=Decision.ALLOW,
278
- reason=f"Allowed by policies: {response.reason}",
279
- )
282
+ return self._convert_cedar_response_to_adjudication(response)
280
283
 
281
- annotations: list[PolicyAnnotation] = []
282
- hard_deny_ids = []
284
+ def _convert_cedar_response_to_adjudication(
285
+ self, response: Response
286
+ ) -> Adjudication:
287
+ escalate_policies = []
288
+ non_escalate_policies = []
283
289
  for internal_id in response.reason:
284
290
  policy = self._policy_set.policy(internal_id)
285
291
  if policy is None:
286
292
  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)
293
+ cedar_policy_annotations = policy.annotations()
294
+ non_default_annotations = {
295
+ k: v
296
+ for k, v in cedar_policy_annotations.items()
297
+ if k not in ("id", "reason", "escalate")
298
+ }
299
+ is_escalate = "escalate" in cedar_policy_annotations
300
+ policy_metadata = PolicyMetadata(
301
+ id=cedar_policy_annotations["id"],
302
+ description=cedar_policy_annotations.get("reason", ""),
303
+ escalate=is_escalate,
304
+ escalate_arg=cedar_policy_annotations.get("escalate", ""),
305
+ custom=non_default_annotations,
306
+ )
307
+ if is_escalate:
308
+ escalate_policies.append(policy_metadata)
290
309
  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
- )
310
+ non_escalate_policies.append(policy_metadata)
305
311
 
306
- if not hard_deny_ids and annotations:
312
+ if str(response.decision) == "Allow":
313
+ # The @escalate annotation is only valid for `forbid` Cedar policies, so we can
314
+ # just return the non-escalate policies here:
307
315
  return Adjudication(
308
- decision=Decision.ESCALATE,
309
- reason=f"Escalated by policies: {response.reason}",
310
- annotations=annotations,
316
+ decision=Decision.ALLOW,
317
+ reason="Allowed by all policies",
318
+ policies=non_escalate_policies,
311
319
  )
312
- else:
320
+ # At this point we know the Cedar decision was DENY, so we just need to figure out if the
321
+ # final decision should be a hard DENY or else ESCALATE.
322
+ if non_escalate_policies:
313
323
  return Adjudication(
314
324
  decision=Decision.DENY,
315
- reason=f"Denied by policies: {hard_deny_ids}",
325
+ reason="Denied by policies",
326
+ policies=non_escalate_policies,
316
327
  )
328
+ if escalate_policies:
329
+ return Adjudication(
330
+ decision=Decision.ESCALATE,
331
+ reason="Escalated by policies",
332
+ policies=escalate_policies,
333
+ )
334
+ # Default deny because no policies matched (neither permit nor forbid/escalate)
335
+ return Adjudication(
336
+ decision=Decision.DENY,
337
+ reason="No matching permit policy",
338
+ policies=[],
339
+ )
317
340
 
318
341
  def _message_request(
319
342
  self,
@@ -16,8 +16,8 @@ from sondera.types import (
16
16
  Decision,
17
17
  GuardrailContext,
18
18
  Parameter,
19
- PolicyAnnotation,
20
19
  PolicyEngineMode,
20
+ PolicyMetadata,
21
21
  PromptContent,
22
22
  Role,
23
23
  SourceCode,
@@ -127,9 +127,9 @@ def _convert_pb_adjudication_to_sdk(
127
127
  primitives_pb2.DECISION_ESCALATE: Decision.ESCALATE,
128
128
  }
129
129
 
130
- # Convert annotations
131
- annotations = [
132
- PolicyAnnotation(
130
+ # Convert annotations to PolicyMetadata
131
+ policies = [
132
+ PolicyMetadata(
133
133
  id=ann.id if ann.HasField("id") else "",
134
134
  description=ann.description if ann.HasField("description") else "",
135
135
  custom=dict(ann.custom),
@@ -140,8 +140,7 @@ def _convert_pb_adjudication_to_sdk(
140
140
  return Adjudication(
141
141
  decision=decision_map[adjudication.decision],
142
142
  reason=adjudication.reason,
143
- policy_ids=list(adjudication.policy_ids),
144
- annotations=annotations,
143
+ policies=policies,
145
144
  )
146
145
 
147
146
 
@@ -562,30 +562,6 @@ class SonderaRemoteHarness(AbstractHarness):
562
562
  )
563
563
  raise
564
564
 
565
- async def _list_trajectory_steps(
566
- self, trajectory_id: str
567
- ) -> list[primitives_pb2.AdjudicatedStep]:
568
- """List trajectory steps internally."""
569
- request = harness_pb2.GetTrajectoryRequest(
570
- trajectory_id=trajectory_id,
571
- )
572
- await self._ensure_connected()
573
- assert self._stub is not None, "Client not connected"
574
-
575
- # Inject organization_id and auth metadata
576
- metadata = self._get_metadata()
577
-
578
- try:
579
- response = await self._stub.GetTrajectory(request, metadata=metadata)
580
- return list(response.steps)
581
- except grpc.aio.AioRpcError as e:
582
- if e.code() == grpc.StatusCode.NOT_FOUND:
583
- return []
584
- logging.error(
585
- f"Failed to list trajectory steps for {trajectory_id}: {e.code()} - {e.details()}"
586
- )
587
- raise
588
-
589
565
  async def _list_agents(
590
566
  self,
591
567
  provider_id: str | None = None,
@@ -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": [
@@ -53,20 +53,22 @@ class ViolationPanel(Widget):
53
53
  with Container(classes="reason-container"):
54
54
  yield Markdown(record.adjudication.reason, classes="reason-text")
55
55
 
56
- # Annotations section
57
- annotations = record.adjudication.annotations
58
- if annotations:
59
- yield Static("[bold]Policy Annotations[/bold]", classes="section-header")
56
+ # Policies section
57
+ policies = record.adjudication.policies
58
+ if policies:
59
+ yield Static("[bold]Policies[/bold]", classes="section-header")
60
60
  with VerticalScroll(classes="annotations-container"):
61
- for ann in annotations:
61
+ for policy in policies:
62
62
  with Container(classes="annotation-card"):
63
- yield Static(f"[bold]{ann.id}[/bold]", classes="annotation-id")
64
- if ann.description:
63
+ yield Static(
64
+ f"[bold]{policy.id}[/bold]", classes="annotation-id"
65
+ )
66
+ if policy.description:
65
67
  yield Static(
66
- ann.description, classes="annotation-description"
68
+ policy.description, classes="annotation-description"
67
69
  )
68
- if ann.custom:
70
+ if policy.custom:
69
71
  with Grid(classes="annotation-custom-grid"):
70
- for key, value in ann.custom.items():
72
+ for key, value in policy.custom.items():
71
73
  yield Static(f"{key}:", classes="label")
72
74
  yield Static(value, classes="value")
@@ -44,15 +44,13 @@ class ViolationsList(Widget):
44
44
  reason = record.adjudication.reason
45
45
  reason_display = reason[:40] + "..." if len(reason) > 43 else reason
46
46
 
47
- # Format annotations as comma-separated policy IDs
48
- annotations = record.adjudication.annotations
49
- if annotations:
50
- ann_ids = [ann.id for ann in annotations]
51
- annotations_display = ", ".join(ann_ids)
52
- if len(annotations_display) > 30:
53
- annotations_display = annotations_display[:27] + "..."
47
+ # Format policies as comma-separated policy IDs
48
+ if record.adjudication.policies:
49
+ policies_display = ", ".join(p.id for p in record.adjudication.policies)
50
+ if len(policies_display) > 30:
51
+ policies_display = policies_display[:27] + "..."
54
52
  else:
55
- annotations_display = "-"
53
+ policies_display = "-"
56
54
 
57
55
  table.add_row(
58
56
  decision_display,
@@ -66,7 +64,7 @@ class ViolationsList(Widget):
66
64
  if len(record.step_id) > 11
67
65
  else record.step_id,
68
66
  reason_display,
69
- annotations_display,
67
+ policies_display,
70
68
  )
71
69
 
72
70
  def get_selected_adjudication(self) -> AdjudicationRecord | None:
sondera/types.py CHANGED
@@ -231,19 +231,19 @@ class GuardrailContext(Model):
231
231
  """Map of check name to check result."""
232
232
 
233
233
 
234
- class PolicyAnnotation(Model):
235
- """Annotation from a policy evaluation."""
234
+ class PolicyMetadata(Model):
235
+ """Metadata about a policy that contributed to an adjudication decision."""
236
236
 
237
237
  id: str
238
- """Unique identifier of the policy that produced this annotation."""
238
+ """Unique identifier of the policy."""
239
239
  description: str
240
- """Human-readable description of why this annotation was added."""
240
+ """Human-readable description from the policy's @reason annotation."""
241
241
  escalate: bool = False
242
242
  """Whether this policy requires escalation to a human or other oracle to decide the final verdict."""
243
243
  escalate_arg: str = ""
244
244
  """The argument passed to @escalate, if any."""
245
245
  custom: dict[str, str] = Field(default_factory=dict)
246
- """Custom key-value metadata from the policy."""
246
+ """Custom key-value metadata from the policy's annotations."""
247
247
 
248
248
 
249
249
  class Adjudication(Model):
@@ -253,25 +253,9 @@ class Adjudication(Model):
253
253
  """Whether the input is allowed."""
254
254
  reason: str
255
255
  """Reason for the adjudication decision."""
256
- policy_ids: list[str] = Field(default_factory=list)
257
- """IDs of policies that contributed to this decision."""
258
- annotations: list[PolicyAnnotation] = Field(default_factory=list)
259
- """Annotations from policy evaluations."""
260
-
261
- @property
262
- def is_denied(self) -> bool:
263
- """Check if is denied."""
264
- return self.decision == Decision.DENY
265
-
266
- @property
267
- def is_allowed(self) -> bool:
268
- """Check if allowed."""
269
- return self.decision == Decision.ALLOW
270
-
271
- @property
272
- def is_escalated(self) -> bool:
273
- """Check if result requires escalation."""
274
- return self.decision == Decision.ESCALATE
256
+ policies: list[PolicyMetadata] = Field(default_factory=list)
257
+ """Policies that determined this decision. Each entry contains the policy's
258
+ id, description (from @reason annotation), escalation info, and custom metadata."""
275
259
 
276
260
 
277
261
  class AdjudicatedStep(Model):
@@ -286,27 +270,6 @@ class AdjudicatedStep(Model):
286
270
  guardrails: GuardrailContext | None = None
287
271
  """Guardrail check results for this step."""
288
272
 
289
- @property
290
- def is_denied(self) -> bool:
291
- """Check if result is denied."""
292
- return (
293
- self.adjudication.decision == Decision.DENY
294
- and self.mode == PolicyEngineMode.GOVERN
295
- )
296
-
297
- @property
298
- def is_allowed(self) -> bool:
299
- """Check if result is allowed."""
300
- return self.adjudication.decision == Decision.ALLOW
301
-
302
- @property
303
- def is_escalated(self) -> bool:
304
- """Check if result requires escalation."""
305
- return (
306
- self.adjudication.decision == Decision.ESCALATE
307
- and self.mode == PolicyEngineMode.GOVERN
308
- )
309
-
310
273
  @property
311
274
  def message(self) -> str:
312
275
  """Get the adjudication reason in a friendly format."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sondera-harness
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Sondera Harness SDK for Python - Agent governance and policy enforcement
5
5
  Author-email: Sondera AI <sdk@sondera.ai>
6
6
  License-Expression: MIT
@@ -27,7 +27,6 @@ Requires-Dist: cedar-python>=0.1.1
27
27
  Requires-Dist: click>=8.0.0
28
28
  Requires-Dist: click-default-group>=1.2.4
29
29
  Requires-Dist: grpcio>=1.76.0
30
- Requires-Dist: grpcio-tools>=1.76.0
31
30
  Requires-Dist: httpx>=0.27.0
32
31
  Requires-Dist: pydantic>=2.12.0
33
32
  Requires-Dist: pydantic-settings>=2.12.0
@@ -47,11 +46,6 @@ Requires-Dist: strands-agents>=1.21.0; extra == "all"
47
46
  Dynamic: license-file
48
47
 
49
48
  <div align="center">
50
- <picture>
51
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-dark.svg">
52
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-light.svg">
53
- <img alt="Sondera" src="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-light.svg" height="60">
54
- </picture>
55
49
 
56
50
  <h1>Sondera Harness</h1>
57
51
 
@@ -66,7 +60,7 @@ Dynamic: license-file
66
60
  ·
67
61
  <a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
68
62
  ·
69
- <a href="https://discord.gg/8zMbcnDnZs">Discord</a>
63
+ <a href="https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw">Slack</a>
70
64
  </p>
71
65
 
72
66
  <p>
@@ -94,12 +88,16 @@ This policy stops your agent from running `rm -rf`, every time.
94
88
 
95
89
  ## Quickstart
96
90
 
91
+ > **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.
92
+
97
93
  ### 1. Install
98
94
 
99
95
  ```bash
100
96
  uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
101
97
  ```
102
98
 
99
+ 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/).
100
+
103
101
  ### 2. Add to Your Agent (LangGraph)
104
102
 
105
103
  ```python
@@ -132,8 +130,6 @@ agent = create_agent(
132
130
  )
133
131
  ```
134
132
 
135
- Also supports [Google ADK](https://docs.sondera.ai/integrations/adk/), [Strands](https://docs.sondera.ai/integrations/strands/), and [custom integrations](https://docs.sondera.ai/integrations/custom/).
136
-
137
133
  > [!NOTE]
138
134
  > 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/).
139
135
 
@@ -163,7 +159,7 @@ uv run sondera # or: sondera (if installed via pip)
163
159
 
164
160
  ## Community
165
161
 
166
- - [Discord](https://discord.gg/8zMbcnDnZs) for questions and feedback
162
+ - [Slack](https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw) for questions and feedback
167
163
  - [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
168
164
  - [Contributing](CONTRIBUTING.md) for development setup
169
165
 
@@ -1,26 +1,26 @@
1
- sondera/__init__.py,sha256=0Z1R6ZkA5VSE_3ZCCgfeecm5nU6mFBdl4-18b8Nwt3Y,3558
1
+ sondera/__init__.py,sha256=0R-C5PfsUUpgW4gFh8NHY-E7U_Vdlteg_Bek9yxlPew,3554
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=t4TbVcieoPgGr9wje0jz0eFmXPmfOvdqsKds8aKgMDI,10428
7
+ sondera/types.py,sha256=XnA4kkP65rIChbUTuZcjY9XIq7R5rEzdZ2kqs5wUS10,9420
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=ioxJCRaAC5Rt73h_Y6G3LgHSkz28K1hVi8nZGyCdAgc,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=ZTUa1TuzNBLESkLMZZu_jAVOnqMOYLGgD1RbylqbhMk,15597
14
+ sondera/harness/cedar/harness.py,sha256=kxid5IlIrHk9tZhF_R_F7WedlnJ4CS-QJKrkZ3X-ZpE,16671
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
- sondera/harness/sondera/_grpc.py,sha256=vLVJwJBtDwX-p8qZPfShpChKxdP3lSFoTV6QiL4dh7s,14708
18
- sondera/harness/sondera/harness.py,sha256=Xg0zc6DZbYvAVceJMlC8fL6D-qX8J-xI1OqPHb-Taxk,31231
17
+ sondera/harness/sondera/_grpc.py,sha256=XnDKFAoBN-W1AiQb5xdC3KmSjZLUifWeg_fXG7NG0mw,14663
18
+ sondera/harness/sondera/harness.py,sha256=caqRvjtIvPF96AhYC00uhboYPIv9p1Ay0WfDCOoKMAA,30352
19
19
  sondera/langgraph/__init__.py,sha256=F2eNoPp944tvGbf9a4etNh3-o49WOPrcs9EFVT0OYYw,473
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
@@ -67,12 +67,12 @@ sondera/tui/widgets/recent_adjudications.py,sha256=6v_ft8Z3RqLYelkxxLWtpiiFfTlP3
67
67
  sondera/tui/widgets/recent_trajectories.py,sha256=phP1Ks9-NLbVWUBtAyb7isgWDJscrtKwzYPLstriZmM,1882
68
68
  sondera/tui/widgets/summary.py,sha256=2HSgJ1Qo_femjweguSayVsNQDaoIYA2t7N-ETGTUt_g,2768
69
69
  sondera/tui/widgets/tool_card.py,sha256=3yNQcc_umcan4V1S5GiEKd7l4YA_atibwn3HF0n6LiY,1151
70
- sondera/tui/widgets/violation_panel.py,sha256=fowe4KWb13NXLX0_RAxEPdRqYeyGzlImpRs4_L9y1zI,2933
71
- sondera/tui/widgets/violations_list.py,sha256=86qICAsQOC6kjQLs64WxK7u59vEJ8kvfiToLVlzFyHM,2866
70
+ sondera/tui/widgets/violation_panel.py,sha256=GaMfc1YtyzW4n_T6JxMYa4GBJMvGTUpY9Y3Jgs6-Uhw,2980
71
+ sondera/tui/widgets/violations_list.py,sha256=N-dsChu_KTYgKgVUGN-IAMZIzp4Yd01e_dlhJha8MNA,2781
72
72
  sondera/tui/widgets/violations_summary.py,sha256=e2LwqlB1aS8sZ2gEC5clk7siA16NSgePU1mpv8T1iTc,4473
73
- sondera_harness-0.6.3.dist-info/licenses/LICENSE,sha256=DmSfauhgrslTxZOcDAmcYqsqsKBkMqVh3PYdjPghNbU,1070
74
- sondera_harness-0.6.3.dist-info/METADATA,sha256=cWb8iOVxQLKyIRcNq8902znCu6oqPr8IRhU6Yvm64lg,6419
75
- sondera_harness-0.6.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
76
- sondera_harness-0.6.3.dist-info/entry_points.txt,sha256=5cLgW0-GzEwNnQjXIhGT21iFprQb1lftBaFJjC4IrgE,78
77
- sondera_harness-0.6.3.dist-info/top_level.txt,sha256=BR0X8Gq9CCpwbQg5evpQfy5zwp9fTuGnlJhXSNqQ_hA,8
78
- sondera_harness-0.6.3.dist-info/RECORD,,
73
+ sondera_harness-0.7.1.dist-info/licenses/LICENSE,sha256=DmSfauhgrslTxZOcDAmcYqsqsKBkMqVh3PYdjPghNbU,1070
74
+ sondera_harness-0.7.1.dist-info/METADATA,sha256=-hQVaVdSoGE7VijwameFC_LVZANr9DC4OjJcIcl2qdo,6325
75
+ sondera_harness-0.7.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
76
+ sondera_harness-0.7.1.dist-info/entry_points.txt,sha256=5cLgW0-GzEwNnQjXIhGT21iFprQb1lftBaFJjC4IrgE,78
77
+ sondera_harness-0.7.1.dist-info/top_level.txt,sha256=BR0X8Gq9CCpwbQg5evpQfy5zwp9fTuGnlJhXSNqQ_hA,8
78
+ sondera_harness-0.7.1.dist-info/RECORD,,