sondera-harness 0.6.2__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.
- sondera_harness-0.6.3/PKG-INFO +172 -0
- sondera_harness-0.6.3/README.md +124 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/pyproject.toml +11 -2
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/__init__.py +35 -15
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/adk/plugin.py +1 -1
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/cedar/harness.py +90 -19
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/types.py +4 -0
- sondera_harness-0.6.3/src/sondera_harness.egg-info/PKG-INFO +172 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/entry_points.txt +1 -0
- sondera_harness-0.6.2/PKG-INFO +0 -323
- sondera_harness-0.6.2/README.md +0 -275
- sondera_harness-0.6.2/src/sondera_harness.egg-info/PKG-INFO +0 -323
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/LICENSE +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/setup.cfg +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/__main__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/adk/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/adk/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/cli.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/exceptions.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/abc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/cedar/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/cedar/schema.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/sondera/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/sondera/_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/harness/sondera/harness.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/langgraph/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/langgraph/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/langgraph/exceptions.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/langgraph/graph.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/langgraph/middleware.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/any_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/duration_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/empty_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/struct_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/timestamp_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/google/protobuf/wrappers_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/primitives_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/primitives_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/py.typed +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/settings.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/strands/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/strands/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/strands/harness.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/app.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/app.tcss +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/screens/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/screens/adjudication.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/screens/agent.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/screens/trajectory.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/agent_card.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/agent_list.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/recent_adjudications.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/recent_trajectories.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/summary.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/tool_card.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violation_panel.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violations_list.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera/tui/widgets/violations_summary.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/SOURCES.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/dependency_links.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/requires.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.6.3}/src/sondera_harness.egg-info/top_level.txt +0 -0
- {sondera_harness-0.6.2 → 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.
|
|
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 = [
|
|
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
|
|
1
|
+
"""Sondera Harness - Steer agents with rules, not prompts.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Wrap your agent, write Cedar policies, ship with confidence. When a policy
|
|
4
|
+
denies an action, the agent gets the reason why and adjusts. Agents self-correct
|
|
5
|
+
instead of failing. This is steering, not just blocking.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Same input, same verdict. Rules are deterministic, not probabilistic. Stop
|
|
8
|
+
debugging prompts and start writing policies.
|
|
9
|
+
|
|
10
|
+
Why Sondera Harness:
|
|
11
|
+
- Steer, don't just block: Denied actions include explanations
|
|
12
|
+
- Drop-in integration: Native middleware for LangGraph, ADK, Strands
|
|
13
|
+
- Full observability: Trajectories capture every action and decision
|
|
14
|
+
- Deterministic rules: Same input, same verdict, every time
|
|
15
|
+
- Ship faster: Reliability, safety, security, and compliance built in
|
|
16
|
+
|
|
17
|
+
Harness Implementations:
|
|
18
|
+
- CedarPolicyHarness: Local evaluation, no network calls, no dependencies
|
|
19
|
+
- SonderaRemoteHarness: Team policies, dashboards, centralized audit logs
|
|
10
20
|
|
|
11
21
|
Framework Integrations:
|
|
12
|
-
- sondera.langgraph: LangGraph
|
|
22
|
+
- sondera.langgraph: LangGraph middleware
|
|
13
23
|
- sondera.adk: Google ADK plugin
|
|
14
|
-
- sondera.strands: Strands
|
|
24
|
+
- sondera.strands: Strands lifecycle hooks
|
|
15
25
|
|
|
16
26
|
Example:
|
|
17
|
-
>>> from sondera import
|
|
18
|
-
>>> harness
|
|
27
|
+
>>> from sondera import CedarPolicyHarness, Agent, Tool
|
|
28
|
+
>>> from sondera.harness.cedar.schema import agent_to_cedar_schema
|
|
29
|
+
>>>
|
|
19
30
|
>>> agent = Agent(
|
|
20
31
|
... id="my-agent",
|
|
21
|
-
... provider_id="
|
|
22
|
-
... name="
|
|
32
|
+
... provider_id="local",
|
|
33
|
+
... name="My_Agent",
|
|
23
34
|
... description="A helpful assistant",
|
|
24
|
-
... instruction="
|
|
25
|
-
... tools=[],
|
|
35
|
+
... instruction="Help users with tasks",
|
|
36
|
+
... tools=[Tool(name="Bash", description="Run commands", parameters=[])],
|
|
37
|
+
... )
|
|
38
|
+
>>> policy = "permit(principal, action, resource);"
|
|
39
|
+
>>> harness = CedarPolicyHarness(
|
|
40
|
+
... policy_set=policy,
|
|
41
|
+
... schema=agent_to_cedar_schema(agent),
|
|
26
42
|
... )
|
|
27
43
|
>>> await harness.initialize(agent=agent)
|
|
28
44
|
"""
|
|
@@ -47,10 +63,12 @@ from sondera.types import (
|
|
|
47
63
|
AdjudicatedStep,
|
|
48
64
|
AdjudicatedTrajectory,
|
|
49
65
|
Adjudication,
|
|
66
|
+
AdjudicationRecord,
|
|
50
67
|
Agent,
|
|
51
68
|
Content,
|
|
52
69
|
Decision,
|
|
53
70
|
Parameter,
|
|
71
|
+
PolicyAnnotation,
|
|
54
72
|
PolicyEngineMode,
|
|
55
73
|
PromptContent,
|
|
56
74
|
Role,
|
|
@@ -93,6 +111,8 @@ __all__ = [
|
|
|
93
111
|
"Adjudication",
|
|
94
112
|
"AdjudicatedStep",
|
|
95
113
|
"AdjudicatedTrajectory",
|
|
114
|
+
"AdjudicationRecord",
|
|
115
|
+
"PolicyAnnotation",
|
|
96
116
|
"Decision",
|
|
97
117
|
# Exceptions
|
|
98
118
|
"SonderaError",
|
|
@@ -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.
|
|
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 =
|
|
88
|
+
self._agent: Agent | None = agent
|
|
86
89
|
self._trajectory_id: str | None = None
|
|
87
90
|
self._trajectory_step_count: int = 0
|
|
88
91
|
self._logger = logger or _LOGGER
|
|
@@ -101,6 +104,16 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
101
104
|
self._policy_set = PolicySet(policy_set)
|
|
102
105
|
else:
|
|
103
106
|
self._policy_set = policy_set
|
|
107
|
+
|
|
108
|
+
for policy in self._policy_set.policies():
|
|
109
|
+
annotations = policy.annotations()
|
|
110
|
+
if "escalate" in annotations and str(policy.effect()) != "Forbid":
|
|
111
|
+
policy_id = annotations.get("id", policy.id())
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Policy '{policy_id}' has @escalate but is not a forbid policy. "
|
|
114
|
+
"@escalate is only valid on forbid policies."
|
|
115
|
+
)
|
|
116
|
+
|
|
104
117
|
# Extract namespace name from schema
|
|
105
118
|
namespaces = list(schema.root.keys())
|
|
106
119
|
if namespaces:
|
|
@@ -110,6 +123,8 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
110
123
|
raise ValueError("Schema must have at least one namespace")
|
|
111
124
|
# Authorizer will be initialized with entities when agent is set
|
|
112
125
|
self._authorizer: Authorizer | None = None
|
|
126
|
+
# Cache for pre-parsed tool response schemas (tool_name -> parsed schema dict)
|
|
127
|
+
self._tool_response_schemas: dict[str, dict[str, object]] = {}
|
|
113
128
|
|
|
114
129
|
def _build_authorizer(self) -> Authorizer:
|
|
115
130
|
"""Build the Cedar authorizer with current entities."""
|
|
@@ -128,8 +143,9 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
128
143
|
if self._agent:
|
|
129
144
|
agent_uid = EntityUid(f"{self._namespace}::Agent", self._agent.id)
|
|
130
145
|
|
|
131
|
-
# Add tool entities from agent's tools
|
|
146
|
+
# Add tool entities from agent's tools and pre-parse response schemas
|
|
132
147
|
tool_entities: list[EntityUid] = []
|
|
148
|
+
self._tool_response_schemas = {}
|
|
133
149
|
for tool in self._agent.tools:
|
|
134
150
|
tool_id = tool.id or tool.name
|
|
135
151
|
tool_uid = EntityUid(f"{self._namespace}::Tool", tool_id)
|
|
@@ -142,6 +158,11 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
142
158
|
)
|
|
143
159
|
tool_entities.append(tool_uid)
|
|
144
160
|
entities.append(tool_entity)
|
|
161
|
+
# Pre-parse response JSON schema for use in _tool_response
|
|
162
|
+
if tool.response_json_schema:
|
|
163
|
+
self._tool_response_schemas[tool.name] = json.loads(
|
|
164
|
+
tool.response_json_schema
|
|
165
|
+
)
|
|
145
166
|
|
|
146
167
|
agent_entity = Entity(
|
|
147
168
|
agent_uid,
|
|
@@ -252,11 +273,47 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
252
273
|
assert request is not None, "Unexpected none request"
|
|
253
274
|
response = self._authorizer.is_authorized(request, self._policy_set)
|
|
254
275
|
if str(response.decision) == "Allow":
|
|
255
|
-
|
|
256
|
-
|
|
276
|
+
return Adjudication(
|
|
277
|
+
decision=Decision.ALLOW,
|
|
278
|
+
reason=f"Allowed by policies: {response.reason}",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
annotations: list[PolicyAnnotation] = []
|
|
282
|
+
hard_deny_ids = []
|
|
283
|
+
for internal_id in response.reason:
|
|
284
|
+
policy = self._policy_set.policy(internal_id)
|
|
285
|
+
if policy is None:
|
|
286
|
+
raise RuntimeError(f"Policy '{internal_id}' not found in policy set")
|
|
287
|
+
policy_annotations = policy.annotations()
|
|
288
|
+
if "escalate" not in policy_annotations:
|
|
289
|
+
hard_deny_ids.append(internal_id)
|
|
290
|
+
else:
|
|
291
|
+
custom = {
|
|
292
|
+
k: v
|
|
293
|
+
for k, v in policy_annotations.items()
|
|
294
|
+
if k not in ("id", "reason", "escalate")
|
|
295
|
+
}
|
|
296
|
+
annotations.append(
|
|
297
|
+
PolicyAnnotation(
|
|
298
|
+
id=policy_annotations.get("id", internal_id),
|
|
299
|
+
description=policy_annotations.get("reason", ""),
|
|
300
|
+
escalate=True,
|
|
301
|
+
escalate_arg=policy_annotations["escalate"],
|
|
302
|
+
custom=custom,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if not hard_deny_ids and annotations:
|
|
307
|
+
return Adjudication(
|
|
308
|
+
decision=Decision.ESCALATE,
|
|
309
|
+
reason=f"Escalated by policies: {response.reason}",
|
|
310
|
+
annotations=annotations,
|
|
311
|
+
)
|
|
257
312
|
else:
|
|
258
|
-
|
|
259
|
-
|
|
313
|
+
return Adjudication(
|
|
314
|
+
decision=Decision.DENY,
|
|
315
|
+
reason=f"Denied by policies: {hard_deny_ids}",
|
|
316
|
+
)
|
|
260
317
|
|
|
261
318
|
def _message_request(
|
|
262
319
|
self,
|
|
@@ -311,11 +368,17 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
311
368
|
action_name = tool_id.replace(" ", "_").replace("-", "_")
|
|
312
369
|
action_uid = EntityUid(f"{self._namespace}::Action", action_name)
|
|
313
370
|
|
|
314
|
-
context
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
371
|
+
# Build context - only include typed parameters if schema defines them
|
|
372
|
+
context_data: dict[str, object] = {
|
|
373
|
+
"parameters_json": json.dumps(content.args),
|
|
374
|
+
}
|
|
375
|
+
# Check if tool has typed parameters schema
|
|
376
|
+
if self._agent:
|
|
377
|
+
tool = next((t for t in self._agent.tools if t.name == tool_id), None)
|
|
378
|
+
if tool and tool.parameters_json_schema:
|
|
379
|
+
context_data["parameters"] = content.args
|
|
380
|
+
|
|
381
|
+
context = Context(context_data, schema=self._schema, action=action_uid)
|
|
319
382
|
|
|
320
383
|
return Request(
|
|
321
384
|
principal=agent_uid,
|
|
@@ -345,14 +408,22 @@ class CedarPolicyHarness(AbstractHarness):
|
|
|
345
408
|
action_name = tool_id.replace(" ", "_").replace("-", "_")
|
|
346
409
|
action_uid = EntityUid(f"{self._namespace}::Action", action_name)
|
|
347
410
|
|
|
348
|
-
context
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
411
|
+
# Build context - only include typed response if schema defines it
|
|
412
|
+
context_data: dict[str, object] = {
|
|
413
|
+
"response_json": json.dumps(content.response, default=str),
|
|
414
|
+
}
|
|
415
|
+
# Check if tool has typed response schema (pre-parsed in _build_authorizer)
|
|
416
|
+
if tool_id in self._tool_response_schemas:
|
|
417
|
+
response_schema = self._tool_response_schemas[tool_id]
|
|
418
|
+
# Check if the response schema is a simple type (not object/Record)
|
|
419
|
+
# Simple types get wrapped in {"value": ...} by the schema generator
|
|
420
|
+
if response_schema.get("type") not in ["object", "OBJECT"]:
|
|
421
|
+
# Simple type was wrapped in {"value": ...} by schema generator
|
|
422
|
+
context_data["response"] = {"value": content.response}
|
|
423
|
+
else:
|
|
424
|
+
context_data["response"] = content.response
|
|
425
|
+
|
|
426
|
+
context = Context(context_data, schema=self._schema, action=action_uid)
|
|
356
427
|
|
|
357
428
|
return Request(
|
|
358
429
|
principal=agent_uid,
|
|
@@ -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
|
|