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.
- sondera_harness-0.7.0/PKG-INFO +169 -0
- sondera_harness-0.7.0/README.md +121 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/pyproject.toml +11 -2
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/__init__.py +35 -15
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/plugin.py +7 -6
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/harness.py +90 -19
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/middleware.py +6 -5
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/harness.py +5 -4
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/types.py +4 -36
- sondera_harness-0.7.0/src/sondera_harness.egg-info/PKG-INFO +169 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/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.7.0}/LICENSE +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/setup.cfg +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/__main__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/adk/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/cli.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/exceptions.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/abc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/cedar/schema.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/harness/sondera/harness.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/exceptions.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/langgraph/graph.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/any_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/duration_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/empty_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/struct_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/timestamp_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/google/protobuf/wrappers_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2.pyi +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/py.typed +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/settings.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/strands/analyze.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/app.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/app.tcss +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/adjudication.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/agent.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/screens/trajectory.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/__init__.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/agent_card.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/agent_list.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/recent_adjudications.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/recent_trajectories.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/summary.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/tool_card.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violation_panel.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violations_list.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera/tui/widgets/violations_summary.py +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/SOURCES.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/dependency_links.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/requires.txt +0 -0
- {sondera_harness-0.6.2 → sondera_harness-0.7.0}/src/sondera_harness.egg-info/top_level.txt +0 -0
- {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:** [](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:** [](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.
|
|
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 = [
|
|
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",
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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,
|