sondera-harness 0.6.1__tar.gz → 0.6.3__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 (88) hide show
  1. sondera_harness-0.6.3/PKG-INFO +172 -0
  2. sondera_harness-0.6.3/README.md +124 -0
  3. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/pyproject.toml +11 -2
  4. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/__init__.py +35 -15
  5. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/adk/plugin.py +1 -1
  6. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/cedar/harness.py +90 -19
  7. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/sondera/_grpc.py +78 -5
  8. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/sondera/harness.py +3 -18
  9. sondera_harness-0.6.3/src/sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
  10. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/primitives_pb2.pyi +8 -2
  11. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/types.py +26 -0
  12. sondera_harness-0.6.3/src/sondera_harness.egg-info/PKG-INFO +172 -0
  13. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/entry_points.txt +1 -0
  14. sondera_harness-0.6.1/PKG-INFO +0 -323
  15. sondera_harness-0.6.1/README.md +0 -275
  16. sondera_harness-0.6.1/src/sondera/proto/sondera/core/v1/primitives_pb2.py +0 -88
  17. sondera_harness-0.6.1/src/sondera_harness.egg-info/PKG-INFO +0 -323
  18. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/LICENSE +0 -0
  19. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/setup.cfg +0 -0
  20. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/__main__.py +0 -0
  21. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/adk/__init__.py +0 -0
  22. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/adk/analyze.py +0 -0
  23. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/cli.py +0 -0
  24. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/exceptions.py +0 -0
  25. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/__init__.py +0 -0
  26. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/abc.py +0 -0
  27. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/cedar/__init__.py +0 -0
  28. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/cedar/schema.py +0 -0
  29. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/harness/sondera/__init__.py +0 -0
  30. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/langgraph/__init__.py +0 -0
  31. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/langgraph/analyze.py +0 -0
  32. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/langgraph/exceptions.py +0 -0
  33. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/langgraph/graph.py +0 -0
  34. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/langgraph/middleware.py +0 -0
  35. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2.py +0 -0
  36. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2.pyi +0 -0
  37. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2_grpc.py +0 -0
  38. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2.py +0 -0
  39. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2.pyi +0 -0
  40. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2_grpc.py +0 -0
  41. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2.py +0 -0
  42. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2.pyi +0 -0
  43. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2_grpc.py +0 -0
  44. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2.py +0 -0
  45. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2.pyi +0 -0
  46. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2_grpc.py +0 -0
  47. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2.py +0 -0
  48. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2.pyi +0 -0
  49. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2_grpc.py +0 -0
  50. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2.py +0 -0
  51. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2.pyi +0 -0
  52. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2_grpc.py +0 -0
  53. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/__init__.py +0 -0
  54. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/__init__.py +0 -0
  55. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/__init__.py +0 -0
  56. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +0 -0
  57. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/__init__.py +0 -0
  58. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/__init__.py +0 -0
  59. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2.py +0 -0
  60. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2.pyi +0 -0
  61. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +0 -0
  62. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/py.typed +0 -0
  63. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/settings.py +0 -0
  64. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/strands/__init__.py +0 -0
  65. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/strands/analyze.py +0 -0
  66. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/strands/harness.py +0 -0
  67. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/__init__.py +0 -0
  68. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/app.py +0 -0
  69. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/app.tcss +0 -0
  70. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/screens/__init__.py +0 -0
  71. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/screens/adjudication.py +0 -0
  72. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/screens/agent.py +0 -0
  73. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/screens/trajectory.py +0 -0
  74. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/__init__.py +0 -0
  75. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/agent_card.py +0 -0
  76. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/agent_list.py +0 -0
  77. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/recent_adjudications.py +0 -0
  78. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/recent_trajectories.py +0 -0
  79. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/summary.py +0 -0
  80. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/tool_card.py +0 -0
  81. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violation_panel.py +0 -0
  82. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violations_list.py +0 -0
  83. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violations_summary.py +0 -0
  84. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/SOURCES.txt +0 -0
  85. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/dependency_links.txt +0 -0
  86. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/requires.txt +0 -0
  87. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/top_level.txt +0 -0
  88. {sondera_harness-0.6.1 → sondera_harness-0.6.3}/tests/test_harness.py +0 -0
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: sondera-harness
3
+ Version: 0.6.3
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
+ <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
+
56
+ <h1>Sondera Harness</h1>
57
+
58
+ <p><strong>Deterministic guardrails for AI agents.</strong></p>
59
+
60
+ <p>Open-source. Works with LangGraph, ADK, Strands, or any custom agent.</p>
61
+
62
+ <p>
63
+ <a href="https://docs.sondera.ai/">Docs</a>
64
+ ·
65
+ <a href="https://docs.sondera.ai/quickstart/">Quickstart</a>
66
+ ·
67
+ <a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
68
+ ·
69
+ <a href="https://discord.gg/8zMbcnDnZs">Discord</a>
70
+ </p>
71
+
72
+ <p>
73
+ <a href="https://pypi.org/project/sondera-harness/"><img src="https://img.shields.io/pypi/v/sondera-harness.svg" alt="PyPI version"></a>
74
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
75
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/sondera-ai/sondera-harness-python.svg" alt="License: MIT"></a>
76
+ </p>
77
+
78
+ </div>
79
+
80
+ ---
81
+
82
+ ## What is Sondera Harness?
83
+
84
+ 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.
85
+
86
+ **Example policy:**
87
+
88
+ ```cedar
89
+ forbid(principal, action, resource)
90
+ when { context has parameters_json && context.parameters_json like "*rm -rf*" };
91
+ ```
92
+
93
+ This policy stops your agent from running `rm -rf`, every time.
94
+
95
+ ## Quickstart
96
+
97
+ ### 1. Install
98
+
99
+ ```bash
100
+ uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
101
+ ```
102
+
103
+ ### 2. Add to Your Agent (LangGraph)
104
+
105
+ ```python
106
+ from langchain.agents import create_agent
107
+ from sondera.harness import SonderaRemoteHarness
108
+ from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
109
+
110
+ # Analyze your tools and create agent metadata
111
+ sondera_agent = create_agent_from_langchain_tools(
112
+ tools=my_tools,
113
+ agent_id="langchain-agent",
114
+ agent_name="My LangChain Agent",
115
+ agent_description="An agent that helps with tasks",
116
+ )
117
+
118
+ # Create harness with agent
119
+ harness = SonderaRemoteHarness(agent=sondera_agent)
120
+
121
+ # Create middleware
122
+ middleware = SonderaHarnessMiddleware(
123
+ harness=harness,
124
+ strategy=Strategy.BLOCK, # or Strategy.STEER
125
+ )
126
+
127
+ # Create agent with middleware
128
+ agent = create_agent(
129
+ model=my_model,
130
+ tools=my_tools,
131
+ middleware=[middleware],
132
+ )
133
+ ```
134
+
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
+ > [!NOTE]
138
+ > 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
+
140
+ ### 3. See It in Action
141
+
142
+ <div align="center">
143
+ <img src="docs/src/assets/sondera-tui.gif" alt="Sondera TUI" width="700" />
144
+ </div>
145
+
146
+ ```bash
147
+ uv run sondera # or: sondera (if installed via pip)
148
+ ```
149
+
150
+ ## Why Sondera Harness?
151
+
152
+ - **Steer, don't block:** Denied actions include a reason. Return it to the model, and it tries something else.
153
+ - **Deterministic:** Stop debugging prompts. Rules are predictable.
154
+ - **Drop-in integration:** Native middleware for LangGraph, Google ADK, and Strands.
155
+ - **Full observability:** Every action, every decision, every reason. Audit-ready.
156
+
157
+ ## Documentation
158
+
159
+ - [Quickstart](https://docs.sondera.ai/quickstart/)
160
+ - [Writing Policies](https://docs.sondera.ai/writing-policies/)
161
+ - [Integrations](https://docs.sondera.ai/integrations/)
162
+ - [Reference](https://docs.sondera.ai/reference/)
163
+
164
+ ## Community
165
+
166
+ - [Discord](https://discord.gg/8zMbcnDnZs) for questions and feedback
167
+ - [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
168
+ - [Contributing](CONTRIBUTING.md) for development setup
169
+
170
+ ## License
171
+
172
+ [MIT](LICENSE)
@@ -0,0 +1,124 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-light.svg">
5
+ <img alt="Sondera" src="https://raw.githubusercontent.com/sondera-ai/harness-sdk-python/main/assets/sondera-logo-light.svg" height="60">
6
+ </picture>
7
+
8
+ <h1>Sondera Harness</h1>
9
+
10
+ <p><strong>Deterministic guardrails for AI agents.</strong></p>
11
+
12
+ <p>Open-source. Works with LangGraph, ADK, Strands, or any custom agent.</p>
13
+
14
+ <p>
15
+ <a href="https://docs.sondera.ai/">Docs</a>
16
+ ·
17
+ <a href="https://docs.sondera.ai/quickstart/">Quickstart</a>
18
+ ·
19
+ <a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
20
+ ·
21
+ <a href="https://discord.gg/8zMbcnDnZs">Discord</a>
22
+ </p>
23
+
24
+ <p>
25
+ <a href="https://pypi.org/project/sondera-harness/"><img src="https://img.shields.io/pypi/v/sondera-harness.svg" alt="PyPI version"></a>
26
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
27
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/sondera-ai/sondera-harness-python.svg" alt="License: MIT"></a>
28
+ </p>
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ ## What is Sondera Harness?
35
+
36
+ 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.
37
+
38
+ **Example policy:**
39
+
40
+ ```cedar
41
+ forbid(principal, action, resource)
42
+ when { context has parameters_json && context.parameters_json like "*rm -rf*" };
43
+ ```
44
+
45
+ This policy stops your agent from running `rm -rf`, every time.
46
+
47
+ ## Quickstart
48
+
49
+ ### 1. Install
50
+
51
+ ```bash
52
+ uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
53
+ ```
54
+
55
+ ### 2. Add to Your Agent (LangGraph)
56
+
57
+ ```python
58
+ from langchain.agents import create_agent
59
+ from sondera.harness import SonderaRemoteHarness
60
+ from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
61
+
62
+ # Analyze your tools and create agent metadata
63
+ sondera_agent = create_agent_from_langchain_tools(
64
+ tools=my_tools,
65
+ agent_id="langchain-agent",
66
+ agent_name="My LangChain Agent",
67
+ agent_description="An agent that helps with tasks",
68
+ )
69
+
70
+ # Create harness with agent
71
+ harness = SonderaRemoteHarness(agent=sondera_agent)
72
+
73
+ # Create middleware
74
+ middleware = SonderaHarnessMiddleware(
75
+ harness=harness,
76
+ strategy=Strategy.BLOCK, # or Strategy.STEER
77
+ )
78
+
79
+ # Create agent with middleware
80
+ agent = create_agent(
81
+ model=my_model,
82
+ tools=my_tools,
83
+ middleware=[middleware],
84
+ )
85
+ ```
86
+
87
+ 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/).
88
+
89
+ > [!NOTE]
90
+ > 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/).
91
+
92
+ ### 3. See It in Action
93
+
94
+ <div align="center">
95
+ <img src="docs/src/assets/sondera-tui.gif" alt="Sondera TUI" width="700" />
96
+ </div>
97
+
98
+ ```bash
99
+ uv run sondera # or: sondera (if installed via pip)
100
+ ```
101
+
102
+ ## Why Sondera Harness?
103
+
104
+ - **Steer, don't block:** Denied actions include a reason. Return it to the model, and it tries something else.
105
+ - **Deterministic:** Stop debugging prompts. Rules are predictable.
106
+ - **Drop-in integration:** Native middleware for LangGraph, Google ADK, and Strands.
107
+ - **Full observability:** Every action, every decision, every reason. Audit-ready.
108
+
109
+ ## Documentation
110
+
111
+ - [Quickstart](https://docs.sondera.ai/quickstart/)
112
+ - [Writing Policies](https://docs.sondera.ai/writing-policies/)
113
+ - [Integrations](https://docs.sondera.ai/integrations/)
114
+ - [Reference](https://docs.sondera.ai/reference/)
115
+
116
+ ## Community
117
+
118
+ - [Discord](https://discord.gg/8zMbcnDnZs) for questions and feedback
119
+ - [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
120
+ - [Contributing](CONTRIBUTING.md) for development setup
121
+
122
+ ## License
123
+
124
+ [MIT](LICENSE)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sondera-harness"
7
- version = "0.6.1"
7
+ version = "0.6.3"
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"
@@ -46,6 +46,8 @@ Changelog = "https://github.com/sondera-ai/harness-sdk-python/blob/main/CHANGELO
46
46
 
47
47
  [project.scripts]
48
48
  sondera = "sondera.cli:cli"
49
+ sondera-harness = "sondera.cli:cli"
50
+
49
51
 
50
52
  [tool.setuptools.package-data]
51
53
  sondera = ["py.typed", "tui/*.tcss"]
@@ -150,7 +152,14 @@ testpaths = ["tests"]
150
152
  addopts = "-v"
151
153
 
152
154
  [tool.uv.workspace]
153
- members = ["examples/adk", "examples/langgraph", "examples/strands", "examples/archetypes", "examples/cedar"]
155
+ members = [
156
+ "examples/adk",
157
+ "examples/langgraph",
158
+ "examples/strands",
159
+ "examples/archetypes",
160
+ "examples/cedar",
161
+ "docs",
162
+ ]
154
163
 
155
164
  [tool.uv.sources]
156
165
  sondera-archetypes = { workspace = true }
@@ -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",
@@ -64,7 +64,7 @@ class SonderaHarnessPlugin(BasePlugin):
64
64
  plugin = SonderaHarnessPlugin(harness=harness)
65
65
 
66
66
  # Create agent and runner with the plugin
67
- agent = Agent(name="my-agent", model="gemini-2.0-flash", ...)
67
+ agent = Agent(name="my-agent", model="gemini-2.5-flash", ...)
68
68
  runner = Runner(
69
69
  agent=agent,
70
70
  app_name="my-app",
@@ -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,