sondera-harness 0.6.2__tar.gz → 0.7.0__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 (87) hide show
  1. sondera_harness-0.7.0/PKG-INFO +169 -0
  2. sondera_harness-0.7.0/README.md +121 -0
  3. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/pyproject.toml +11 -2
  4. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/__init__.py +35 -15
  5. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/plugin.py +7 -6
  6. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/harness.py +90 -19
  7. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/middleware.py +6 -5
  8. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/harness.py +5 -4
  9. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/types.py +4 -36
  10. sondera_harness-0.7.0/src/sondera_harness.egg-info/PKG-INFO +169 -0
  11. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/entry_points.txt +1 -0
  12. sondera_harness-0.6.2/PKG-INFO +0 -323
  13. sondera_harness-0.6.2/README.md +0 -275
  14. sondera_harness-0.6.2/src/sondera_harness.egg-info/PKG-INFO +0 -323
  15. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/LICENSE +0 -0
  16. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/setup.cfg +0 -0
  17. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/__main__.py +0 -0
  18. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/__init__.py +0 -0
  19. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/analyze.py +0 -0
  20. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/cli.py +0 -0
  21. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/exceptions.py +0 -0
  22. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/__init__.py +0 -0
  23. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/abc.py +0 -0
  24. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/__init__.py +0 -0
  25. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/schema.py +0 -0
  26. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/__init__.py +0 -0
  27. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/_grpc.py +0 -0
  28. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/harness.py +0 -0
  29. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/__init__.py +0 -0
  30. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/analyze.py +0 -0
  31. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/exceptions.py +0 -0
  32. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/graph.py +0 -0
  33. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2.py +0 -0
  34. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2.pyi +0 -0
  35. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2_grpc.py +0 -0
  36. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2.py +0 -0
  37. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2.pyi +0 -0
  38. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2_grpc.py +0 -0
  39. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2.py +0 -0
  40. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2.pyi +0 -0
  41. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2_grpc.py +0 -0
  42. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2.py +0 -0
  43. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2.pyi +0 -0
  44. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2_grpc.py +0 -0
  45. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2.py +0 -0
  46. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2.pyi +0 -0
  47. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2_grpc.py +0 -0
  48. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2.py +0 -0
  49. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2.pyi +0 -0
  50. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2_grpc.py +0 -0
  51. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/__init__.py +0 -0
  52. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/__init__.py +0 -0
  53. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/__init__.py +0 -0
  54. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2.py +0 -0
  55. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2.pyi +0 -0
  56. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +0 -0
  57. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/__init__.py +0 -0
  58. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/__init__.py +0 -0
  59. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2.py +0 -0
  60. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2.pyi +0 -0
  61. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +0 -0
  62. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/py.typed +0 -0
  63. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/settings.py +0 -0
  64. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/__init__.py +0 -0
  65. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/analyze.py +0 -0
  66. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/__init__.py +0 -0
  67. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/app.py +0 -0
  68. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/app.tcss +0 -0
  69. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/__init__.py +0 -0
  70. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/adjudication.py +0 -0
  71. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/agent.py +0 -0
  72. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/trajectory.py +0 -0
  73. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/__init__.py +0 -0
  74. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/agent_card.py +0 -0
  75. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/agent_list.py +0 -0
  76. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/recent_adjudications.py +0 -0
  77. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/recent_trajectories.py +0 -0
  78. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/summary.py +0 -0
  79. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/tool_card.py +0 -0
  80. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violation_panel.py +0 -0
  81. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violations_list.py +0 -0
  82. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violations_summary.py +0 -0
  83. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/SOURCES.txt +0 -0
  84. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/dependency_links.txt +0 -0
  85. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/requires.txt +0 -0
  86. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/top_level.txt +0 -0
  87. {sondera_harness-0.6.2 → sondera_harness-0.7.0}/tests/test_harness.py +0 -0
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: sondera-harness
3
+ Version: 0.7.0
4
+ Summary: Sondera Harness SDK for Python - Agent governance and policy enforcement
5
+ Author-email: Sondera AI <sdk@sondera.ai>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sondera-ai/harness-sdk-python
8
+ Project-URL: Documentation, https://docs.sondera.ai
9
+ Project-URL: Repository, https://github.com/sondera-ai/harness-sdk-python
10
+ Project-URL: Issues, https://github.com/sondera-ai/harness-sdk-python/issues
11
+ Project-URL: Changelog, https://github.com/sondera-ai/harness-sdk-python/blob/main/CHANGELOG.md
12
+ Keywords: ai,agents,governance,policy,guardrails,llm,langchain,langgraph
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: <3.15,>=3.12
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: cedar-python>=0.1.1
27
+ Requires-Dist: click>=8.0.0
28
+ Requires-Dist: click-default-group>=1.2.4
29
+ Requires-Dist: grpcio>=1.76.0
30
+ Requires-Dist: grpcio-tools>=1.76.0
31
+ Requires-Dist: httpx>=0.27.0
32
+ Requires-Dist: pydantic>=2.12.0
33
+ Requires-Dist: pydantic-settings>=2.12.0
34
+ Requires-Dist: textual>=6.11.0
35
+ Provides-Extra: adk
36
+ Requires-Dist: google-adk>=1.22.0; extra == "adk"
37
+ Provides-Extra: langgraph
38
+ Requires-Dist: langchain>=1.2.0; extra == "langgraph"
39
+ Requires-Dist: langgraph>=1.0.5; extra == "langgraph"
40
+ Provides-Extra: strands
41
+ Requires-Dist: strands-agents>=1.21.0; extra == "strands"
42
+ Provides-Extra: all
43
+ Requires-Dist: google-adk>=1.22.0; extra == "all"
44
+ Requires-Dist: langchain>=1.2.0; extra == "all"
45
+ Requires-Dist: langgraph>=1.0.5; extra == "all"
46
+ Requires-Dist: strands-agents>=1.21.0; extra == "all"
47
+ Dynamic: license-file
48
+
49
+ <div align="center">
50
+
51
+ <h1>Sondera Harness</h1>
52
+
53
+ <p><strong>Deterministic guardrails for AI agents.</strong></p>
54
+
55
+ <p>Open-source. Works with LangGraph, ADK, Strands, or any custom agent.</p>
56
+
57
+ <p>
58
+ <a href="https://docs.sondera.ai/">Docs</a>
59
+ ·
60
+ <a href="https://docs.sondera.ai/quickstart/">Quickstart</a>
61
+ ·
62
+ <a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
63
+ ·
64
+ <a href="https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw">Slack</a>
65
+ </p>
66
+
67
+ <p>
68
+ <a href="https://pypi.org/project/sondera-harness/"><img src="https://img.shields.io/pypi/v/sondera-harness.svg" alt="PyPI version"></a>
69
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
70
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/sondera-ai/sondera-harness-python.svg" alt="License: MIT"></a>
71
+ </p>
72
+
73
+ </div>
74
+
75
+ ---
76
+
77
+ ## What is Sondera Harness?
78
+
79
+ Sondera Harness evaluates [Cedar](https://www.cedarpolicy.com/) policies before your agent's actions execute. When a policy denies an action, the agent gets a reason why and can try a different approach. Same input, same verdict. Deterministic, not probabilistic.
80
+
81
+ **Example policy:**
82
+
83
+ ```cedar
84
+ forbid(principal, action, resource)
85
+ when { context has parameters_json && context.parameters_json like "*rm -rf*" };
86
+ ```
87
+
88
+ This policy stops your agent from running `rm -rf`, every time.
89
+
90
+ ## Quickstart
91
+
92
+ > **Try it now:** [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/sondera-ai/sondera-harness-python/blob/main/docs/src/notebooks/quickstart.ipynb) - no install required.
93
+
94
+ ### 1. Install
95
+
96
+ ```bash
97
+ uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
98
+ ```
99
+
100
+ Works with [LangChain/LangGraph](https://docs.sondera.ai/integrations/langgraph/), [Google ADK](https://docs.sondera.ai/integrations/adk/), [Strands](https://docs.sondera.ai/integrations/strands/), and [custom agents](https://docs.sondera.ai/integrations/custom/).
101
+
102
+ ### 2. Add to Your Agent (LangGraph)
103
+
104
+ ```python
105
+ from langchain.agents import create_agent
106
+ from sondera.harness import SonderaRemoteHarness
107
+ from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
108
+
109
+ # Analyze your tools and create agent metadata
110
+ sondera_agent = create_agent_from_langchain_tools(
111
+ tools=my_tools,
112
+ agent_id="langchain-agent",
113
+ agent_name="My LangChain Agent",
114
+ agent_description="An agent that helps with tasks",
115
+ )
116
+
117
+ # Create harness with agent
118
+ harness = SonderaRemoteHarness(agent=sondera_agent)
119
+
120
+ # Create middleware
121
+ middleware = SonderaHarnessMiddleware(
122
+ harness=harness,
123
+ strategy=Strategy.BLOCK, # or Strategy.STEER
124
+ )
125
+
126
+ # Create agent with middleware
127
+ agent = create_agent(
128
+ model=my_model,
129
+ tools=my_tools,
130
+ middleware=[middleware],
131
+ )
132
+ ```
133
+
134
+ > [!NOTE]
135
+ > This example uses Sondera Platform ([free account](https://sondera.ai)), which also enables the TUI below. For local-only development, see [CedarPolicyHarness](https://docs.sondera.ai/integrations/custom/).
136
+
137
+ ### 3. See It in Action
138
+
139
+ <div align="center">
140
+ <img src="docs/src/assets/sondera-tui.gif" alt="Sondera TUI" width="700" />
141
+ </div>
142
+
143
+ ```bash
144
+ uv run sondera # or: sondera (if installed via pip)
145
+ ```
146
+
147
+ ## Why Sondera Harness?
148
+
149
+ - **Steer, don't block:** Denied actions include a reason. Return it to the model, and it tries something else.
150
+ - **Deterministic:** Stop debugging prompts. Rules are predictable.
151
+ - **Drop-in integration:** Native middleware for LangGraph, Google ADK, and Strands.
152
+ - **Full observability:** Every action, every decision, every reason. Audit-ready.
153
+
154
+ ## Documentation
155
+
156
+ - [Quickstart](https://docs.sondera.ai/quickstart/)
157
+ - [Writing Policies](https://docs.sondera.ai/writing-policies/)
158
+ - [Integrations](https://docs.sondera.ai/integrations/)
159
+ - [Reference](https://docs.sondera.ai/reference/)
160
+
161
+ ## Community
162
+
163
+ - [Slack](https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw) for questions and feedback
164
+ - [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
165
+ - [Contributing](CONTRIBUTING.md) for development setup
166
+
167
+ ## License
168
+
169
+ [MIT](LICENSE)
@@ -0,0 +1,121 @@
1
+ <div align="center">
2
+
3
+ <h1>Sondera Harness</h1>
4
+
5
+ <p><strong>Deterministic guardrails for AI agents.</strong></p>
6
+
7
+ <p>Open-source. Works with LangGraph, ADK, Strands, or any custom agent.</p>
8
+
9
+ <p>
10
+ <a href="https://docs.sondera.ai/">Docs</a>
11
+ ·
12
+ <a href="https://docs.sondera.ai/quickstart/">Quickstart</a>
13
+ ·
14
+ <a href="https://github.com/sondera-ai/sondera-harness-python/tree/main/examples">Examples</a>
15
+ ·
16
+ <a href="https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw">Slack</a>
17
+ </p>
18
+
19
+ <p>
20
+ <a href="https://pypi.org/project/sondera-harness/"><img src="https://img.shields.io/pypi/v/sondera-harness.svg" alt="PyPI version"></a>
21
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
22
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/sondera-ai/sondera-harness-python.svg" alt="License: MIT"></a>
23
+ </p>
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## What is Sondera Harness?
30
+
31
+ 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.
32
+
33
+ **Example policy:**
34
+
35
+ ```cedar
36
+ forbid(principal, action, resource)
37
+ when { context has parameters_json && context.parameters_json like "*rm -rf*" };
38
+ ```
39
+
40
+ This policy stops your agent from running `rm -rf`, every time.
41
+
42
+ ## Quickstart
43
+
44
+ > **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.
45
+
46
+ ### 1. Install
47
+
48
+ ```bash
49
+ uv add "sondera-harness[langgraph]" # or: pip install "sondera-harness[langgraph]"
50
+ ```
51
+
52
+ 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/).
53
+
54
+ ### 2. Add to Your Agent (LangGraph)
55
+
56
+ ```python
57
+ from langchain.agents import create_agent
58
+ from sondera.harness import SonderaRemoteHarness
59
+ from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
60
+
61
+ # Analyze your tools and create agent metadata
62
+ sondera_agent = create_agent_from_langchain_tools(
63
+ tools=my_tools,
64
+ agent_id="langchain-agent",
65
+ agent_name="My LangChain Agent",
66
+ agent_description="An agent that helps with tasks",
67
+ )
68
+
69
+ # Create harness with agent
70
+ harness = SonderaRemoteHarness(agent=sondera_agent)
71
+
72
+ # Create middleware
73
+ middleware = SonderaHarnessMiddleware(
74
+ harness=harness,
75
+ strategy=Strategy.BLOCK, # or Strategy.STEER
76
+ )
77
+
78
+ # Create agent with middleware
79
+ agent = create_agent(
80
+ model=my_model,
81
+ tools=my_tools,
82
+ middleware=[middleware],
83
+ )
84
+ ```
85
+
86
+ > [!NOTE]
87
+ > 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/).
88
+
89
+ ### 3. See It in Action
90
+
91
+ <div align="center">
92
+ <img src="docs/src/assets/sondera-tui.gif" alt="Sondera TUI" width="700" />
93
+ </div>
94
+
95
+ ```bash
96
+ uv run sondera # or: sondera (if installed via pip)
97
+ ```
98
+
99
+ ## Why Sondera Harness?
100
+
101
+ - **Steer, don't block:** Denied actions include a reason. Return it to the model, and it tries something else.
102
+ - **Deterministic:** Stop debugging prompts. Rules are predictable.
103
+ - **Drop-in integration:** Native middleware for LangGraph, Google ADK, and Strands.
104
+ - **Full observability:** Every action, every decision, every reason. Audit-ready.
105
+
106
+ ## Documentation
107
+
108
+ - [Quickstart](https://docs.sondera.ai/quickstart/)
109
+ - [Writing Policies](https://docs.sondera.ai/writing-policies/)
110
+ - [Integrations](https://docs.sondera.ai/integrations/)
111
+ - [Reference](https://docs.sondera.ai/reference/)
112
+
113
+ ## Community
114
+
115
+ - [Slack](https://join.slack.com/t/sonderacommunity/shared_invite/zt-3onw10qhj-5UNQ7EMuAbPk0nTwh_sNcw) for questions and feedback
116
+ - [GitHub Issues](https://github.com/sondera-ai/sondera-harness-python/issues) for bugs
117
+ - [Contributing](CONTRIBUTING.md) for development setup
118
+
119
+ ## License
120
+
121
+ [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.2"
7
+ version = "0.7.0"
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",
@@ -23,6 +23,7 @@ from google.genai import types as genai_types
23
23
  from sondera.adk.analyze import format
24
24
  from sondera.harness import Harness
25
25
  from sondera.types import (
26
+ Decision,
26
27
  PromptContent,
27
28
  Role,
28
29
  Stage,
@@ -64,7 +65,7 @@ class SonderaHarnessPlugin(BasePlugin):
64
65
  plugin = SonderaHarnessPlugin(harness=harness)
65
66
 
66
67
  # Create agent and runner with the plugin
67
- agent = Agent(name="my-agent", model="gemini-2.0-flash", ...)
68
+ agent = Agent(name="my-agent", model="gemini-2.5-flash", ...)
68
69
  runner = Runner(
69
70
  agent=agent,
70
71
  app_name="my-app",
@@ -135,7 +136,7 @@ class SonderaHarnessPlugin(BasePlugin):
135
136
  f"[SonderaHarness] User message adjudication for trajectory {self._harness.trajectory_id}"
136
137
  )
137
138
 
138
- if adjudication.is_denied:
139
+ if adjudication.decision == Decision.DENY:
139
140
  return genai_types.Content(
140
141
  parts=[genai_types.Part(text=adjudication.reason)]
141
142
  )
@@ -212,7 +213,7 @@ class SonderaHarnessPlugin(BasePlugin):
212
213
  f"[SonderaHarness] Before model adjudication for trajectory {self._harness.trajectory_id}"
213
214
  )
214
215
 
215
- if adjudication.is_denied:
216
+ if adjudication.decision == Decision.DENY:
216
217
  return LlmResponse(
217
218
  content=genai_types.Content(
218
219
  parts=[genai_types.Part(text=adjudication.reason)]
@@ -254,7 +255,7 @@ class SonderaHarnessPlugin(BasePlugin):
254
255
  f"[SonderaHarness] After model adjudication for trajectory {self._harness.trajectory_id}"
255
256
  )
256
257
 
257
- if adjudication.is_denied:
258
+ if adjudication.decision == Decision.DENY:
258
259
  return LlmResponse(
259
260
  content=genai_types.Content(
260
261
  parts=[genai_types.Part(text=adjudication.reason)]
@@ -296,7 +297,7 @@ class SonderaHarnessPlugin(BasePlugin):
296
297
  f"[SonderaHarness] Before tool adjudication for trajectory {self._harness.trajectory_id}"
297
298
  )
298
299
 
299
- if adjudication.is_denied:
300
+ if adjudication.decision == Decision.DENY:
300
301
  return {"error": f"Tool blocked: {adjudication.reason}"}
301
302
  return None
302
303
 
@@ -332,7 +333,7 @@ class SonderaHarnessPlugin(BasePlugin):
332
333
  f"[SonderaHarness] After tool adjudication for trajectory {self._harness.trajectory_id}"
333
334
  )
334
335
 
335
- if adjudication.is_denied:
336
+ if adjudication.decision == Decision.DENY:
336
337
  return {"error": f"Tool result blocked: {adjudication.reason}"}
337
338
  return None
338
339
 
@@ -23,6 +23,7 @@ from sondera.types import (
23
23
  Agent,
24
24
  Content,
25
25
  Decision,
26
+ PolicyAnnotation,
26
27
  PromptContent,
27
28
  Role,
28
29
  Stage,
@@ -69,6 +70,7 @@ class CedarPolicyHarness(AbstractHarness):
69
70
  *,
70
71
  policy_set: PolicySet | str,
71
72
  schema: CedarSchema,
73
+ agent: Agent | None = None,
72
74
  logger: logging.Logger | None = None,
73
75
  ):
74
76
  """Initialize the Cedar policy engine.
@@ -77,12 +79,13 @@ class CedarPolicyHarness(AbstractHarness):
77
79
  policy_set: Cedar policies to evaluate. Can be a PolicySet instance
78
80
  or Cedar policy text. Required.
79
81
  schema: Cedar schema generated from agent_to_cedar_schema(). Required.
82
+ agent: The agent to govern. Required for adjudication.
80
83
  logger: Logger instance.
81
84
 
82
85
  Raises:
83
86
  ValueError: If policy_set or schema is not provided.
84
87
  """
85
- self._agent: Agent | None = None
88
+ self._agent: Agent | None = agent
86
89
  self._trajectory_id: str | None = None
87
90
  self._trajectory_step_count: int = 0
88
91
  self._logger = logger or _LOGGER
@@ -101,6 +104,16 @@ class CedarPolicyHarness(AbstractHarness):
101
104
  self._policy_set = PolicySet(policy_set)
102
105
  else:
103
106
  self._policy_set = policy_set
107
+
108
+ for policy in self._policy_set.policies():
109
+ annotations = policy.annotations()
110
+ if "escalate" in annotations and str(policy.effect()) != "Forbid":
111
+ policy_id = annotations.get("id", policy.id())
112
+ raise ValueError(
113
+ f"Policy '{policy_id}' has @escalate but is not a forbid policy. "
114
+ "@escalate is only valid on forbid policies."
115
+ )
116
+
104
117
  # Extract namespace name from schema
105
118
  namespaces = list(schema.root.keys())
106
119
  if namespaces:
@@ -110,6 +123,8 @@ class CedarPolicyHarness(AbstractHarness):
110
123
  raise ValueError("Schema must have at least one namespace")
111
124
  # Authorizer will be initialized with entities when agent is set
112
125
  self._authorizer: Authorizer | None = None
126
+ # Cache for pre-parsed tool response schemas (tool_name -> parsed schema dict)
127
+ self._tool_response_schemas: dict[str, dict[str, object]] = {}
113
128
 
114
129
  def _build_authorizer(self) -> Authorizer:
115
130
  """Build the Cedar authorizer with current entities."""
@@ -128,8 +143,9 @@ class CedarPolicyHarness(AbstractHarness):
128
143
  if self._agent:
129
144
  agent_uid = EntityUid(f"{self._namespace}::Agent", self._agent.id)
130
145
 
131
- # Add tool entities from agent's tools
146
+ # Add tool entities from agent's tools and pre-parse response schemas
132
147
  tool_entities: list[EntityUid] = []
148
+ self._tool_response_schemas = {}
133
149
  for tool in self._agent.tools:
134
150
  tool_id = tool.id or tool.name
135
151
  tool_uid = EntityUid(f"{self._namespace}::Tool", tool_id)
@@ -142,6 +158,11 @@ class CedarPolicyHarness(AbstractHarness):
142
158
  )
143
159
  tool_entities.append(tool_uid)
144
160
  entities.append(tool_entity)
161
+ # Pre-parse response JSON schema for use in _tool_response
162
+ if tool.response_json_schema:
163
+ self._tool_response_schemas[tool.name] = json.loads(
164
+ tool.response_json_schema
165
+ )
145
166
 
146
167
  agent_entity = Entity(
147
168
  agent_uid,
@@ -252,11 +273,47 @@ class CedarPolicyHarness(AbstractHarness):
252
273
  assert request is not None, "Unexpected none request"
253
274
  response = self._authorizer.is_authorized(request, self._policy_set)
254
275
  if str(response.decision) == "Allow":
255
- reason = f"Allowed by policies: {response.reason}"
256
- return Adjudication(decision=Decision.ALLOW, reason=reason)
276
+ return Adjudication(
277
+ decision=Decision.ALLOW,
278
+ reason=f"Allowed by policies: {response.reason}",
279
+ )
280
+
281
+ annotations: list[PolicyAnnotation] = []
282
+ hard_deny_ids = []
283
+ for internal_id in response.reason:
284
+ policy = self._policy_set.policy(internal_id)
285
+ if policy is None:
286
+ raise RuntimeError(f"Policy '{internal_id}' not found in policy set")
287
+ policy_annotations = policy.annotations()
288
+ if "escalate" not in policy_annotations:
289
+ hard_deny_ids.append(internal_id)
290
+ else:
291
+ custom = {
292
+ k: v
293
+ for k, v in policy_annotations.items()
294
+ if k not in ("id", "reason", "escalate")
295
+ }
296
+ annotations.append(
297
+ PolicyAnnotation(
298
+ id=policy_annotations.get("id", internal_id),
299
+ description=policy_annotations.get("reason", ""),
300
+ escalate=True,
301
+ escalate_arg=policy_annotations["escalate"],
302
+ custom=custom,
303
+ )
304
+ )
305
+
306
+ if not hard_deny_ids and annotations:
307
+ return Adjudication(
308
+ decision=Decision.ESCALATE,
309
+ reason=f"Escalated by policies: {response.reason}",
310
+ annotations=annotations,
311
+ )
257
312
  else:
258
- reason = f"Denied by policies: {response.reason}"
259
- return Adjudication(decision=Decision.DENY, reason=reason)
313
+ return Adjudication(
314
+ decision=Decision.DENY,
315
+ reason=f"Denied by policies: {hard_deny_ids}",
316
+ )
260
317
 
261
318
  def _message_request(
262
319
  self,
@@ -311,11 +368,17 @@ class CedarPolicyHarness(AbstractHarness):
311
368
  action_name = tool_id.replace(" ", "_").replace("-", "_")
312
369
  action_uid = EntityUid(f"{self._namespace}::Action", action_name)
313
370
 
314
- context = Context(
315
- {"parameters_json": json.dumps(content.args), "parameters": content.args},
316
- schema=self._schema,
317
- action=action_uid,
318
- )
371
+ # Build context - only include typed parameters if schema defines them
372
+ context_data: dict[str, object] = {
373
+ "parameters_json": json.dumps(content.args),
374
+ }
375
+ # Check if tool has typed parameters schema
376
+ if self._agent:
377
+ tool = next((t for t in self._agent.tools if t.name == tool_id), None)
378
+ if tool and tool.parameters_json_schema:
379
+ context_data["parameters"] = content.args
380
+
381
+ context = Context(context_data, schema=self._schema, action=action_uid)
319
382
 
320
383
  return Request(
321
384
  principal=agent_uid,
@@ -345,14 +408,22 @@ class CedarPolicyHarness(AbstractHarness):
345
408
  action_name = tool_id.replace(" ", "_").replace("-", "_")
346
409
  action_uid = EntityUid(f"{self._namespace}::Action", action_name)
347
410
 
348
- context = Context(
349
- {
350
- "response_json": json.dumps(content.response, default=str),
351
- "response": content.response,
352
- },
353
- schema=self._schema,
354
- action=action_uid,
355
- )
411
+ # Build context - only include typed response if schema defines it
412
+ context_data: dict[str, object] = {
413
+ "response_json": json.dumps(content.response, default=str),
414
+ }
415
+ # Check if tool has typed response schema (pre-parsed in _build_authorizer)
416
+ if tool_id in self._tool_response_schemas:
417
+ response_schema = self._tool_response_schemas[tool_id]
418
+ # Check if the response schema is a simple type (not object/Record)
419
+ # Simple types get wrapped in {"value": ...} by the schema generator
420
+ if response_schema.get("type") not in ["object", "OBJECT"]:
421
+ # Simple type was wrapped in {"value": ...} by schema generator
422
+ context_data["response"] = {"value": content.response}
423
+ else:
424
+ context_data["response"] = content.response
425
+
426
+ context = Context(context_data, schema=self._schema, action=action_uid)
356
427
 
357
428
  return Request(
358
429
  principal=agent_uid,