sondera-harness 0.7.0__tar.gz → 0.7.1__tar.gz

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.
Files changed (84) hide show
  1. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/PKG-INFO +1 -2
  2. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/pyproject.toml +2 -2
  3. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/__init__.py +2 -2
  4. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/cedar/harness.py +57 -34
  5. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/sondera/_grpc.py +5 -6
  6. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/sondera/harness.py +0 -24
  7. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/violation_panel.py +12 -10
  8. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/violations_list.py +7 -9
  9. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/types.py +8 -9
  10. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/PKG-INFO +1 -2
  11. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/requires.txt +0 -1
  12. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/LICENSE +0 -0
  13. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/README.md +0 -0
  14. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/setup.cfg +0 -0
  15. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/__main__.py +0 -0
  16. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/adk/__init__.py +0 -0
  17. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/adk/analyze.py +0 -0
  18. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/adk/plugin.py +0 -0
  19. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/cli.py +0 -0
  20. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/exceptions.py +0 -0
  21. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/__init__.py +0 -0
  22. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/abc.py +0 -0
  23. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/cedar/__init__.py +0 -0
  24. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/cedar/schema.py +0 -0
  25. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/harness/sondera/__init__.py +0 -0
  26. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/langgraph/__init__.py +0 -0
  27. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/langgraph/analyze.py +0 -0
  28. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/langgraph/exceptions.py +0 -0
  29. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/langgraph/graph.py +0 -0
  30. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/langgraph/middleware.py +0 -0
  31. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/any_pb2.py +0 -0
  32. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/any_pb2.pyi +0 -0
  33. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/any_pb2_grpc.py +0 -0
  34. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/duration_pb2.py +0 -0
  35. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/duration_pb2.pyi +0 -0
  36. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/duration_pb2_grpc.py +0 -0
  37. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/empty_pb2.py +0 -0
  38. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/empty_pb2.pyi +0 -0
  39. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/empty_pb2_grpc.py +0 -0
  40. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/struct_pb2.py +0 -0
  41. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/struct_pb2.pyi +0 -0
  42. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/struct_pb2_grpc.py +0 -0
  43. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/timestamp_pb2.py +0 -0
  44. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/timestamp_pb2.pyi +0 -0
  45. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/timestamp_pb2_grpc.py +0 -0
  46. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/wrappers_pb2.py +0 -0
  47. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/wrappers_pb2.pyi +0 -0
  48. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/google/protobuf/wrappers_pb2_grpc.py +0 -0
  49. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/__init__.py +0 -0
  50. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/core/__init__.py +0 -0
  51. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/core/v1/__init__.py +0 -0
  52. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/core/v1/primitives_pb2.py +0 -0
  53. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/core/v1/primitives_pb2.pyi +0 -0
  54. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +0 -0
  55. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/harness/__init__.py +0 -0
  56. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/harness/v1/__init__.py +0 -0
  57. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/harness/v1/harness_pb2.py +0 -0
  58. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/harness/v1/harness_pb2.pyi +0 -0
  59. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +0 -0
  60. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/py.typed +0 -0
  61. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/settings.py +0 -0
  62. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/strands/__init__.py +0 -0
  63. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/strands/analyze.py +0 -0
  64. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/strands/harness.py +0 -0
  65. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/__init__.py +0 -0
  66. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/app.py +0 -0
  67. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/app.tcss +0 -0
  68. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/screens/__init__.py +0 -0
  69. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/screens/adjudication.py +0 -0
  70. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/screens/agent.py +0 -0
  71. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/screens/trajectory.py +0 -0
  72. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/__init__.py +0 -0
  73. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/agent_card.py +0 -0
  74. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/agent_list.py +0 -0
  75. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/recent_adjudications.py +0 -0
  76. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/recent_trajectories.py +0 -0
  77. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/summary.py +0 -0
  78. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/tool_card.py +0 -0
  79. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera/tui/widgets/violations_summary.py +0 -0
  80. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/SOURCES.txt +0 -0
  81. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/dependency_links.txt +0 -0
  82. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/entry_points.txt +0 -0
  83. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/src/sondera_harness.egg-info/top_level.txt +0 -0
  84. {sondera_harness-0.7.0 → sondera_harness-0.7.1}/tests/test_harness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sondera-harness
3
- Version: 0.7.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sondera-harness"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Sondera Harness SDK for Python - Agent governance and policy enforcement"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.15"
@@ -30,7 +30,6 @@ dependencies = [
30
30
  "click>=8.0.0",
31
31
  "click-default-group>=1.2.4",
32
32
  "grpcio>=1.76.0",
33
- "grpcio-tools>=1.76.0",
34
33
  "httpx>=0.27.0",
35
34
  "pydantic>=2.12.0",
36
35
  "pydantic-settings>=2.12.0",
@@ -71,6 +70,7 @@ dev = [
71
70
  "textual-dev>=1.8.0",
72
71
  "ruff>=0.8.0",
73
72
  "pyright",
73
+ "grpcio-tools>=1.76.0",
74
74
  ]
75
75
  examples = [
76
76
  "sondera-examples-adk",
@@ -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",
@@ -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,
@@ -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:
@@ -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,10 +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."""
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."""
260
259
 
261
260
 
262
261
  class AdjudicatedStep(Model):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sondera-harness
3
- Version: 0.7.0
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
@@ -2,7 +2,6 @@ cedar-python>=0.1.1
2
2
  click>=8.0.0
3
3
  click-default-group>=1.2.4
4
4
  grpcio>=1.76.0
5
- grpcio-tools>=1.76.0
6
5
  httpx>=0.27.0
7
6
  pydantic>=2.12.0
8
7
  pydantic-settings>=2.12.0
File without changes