sondera-harness 0.6.2__py3-none-any.whl → 0.6.3__py3-none-any.whl
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/__init__.py +35 -15
- sondera/adk/plugin.py +1 -1
- sondera/harness/cedar/harness.py +90 -19
- sondera/types.py +4 -0
- sondera_harness-0.6.3.dist-info/METADATA +172 -0
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.6.3.dist-info}/RECORD +10 -10
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.6.3.dist-info}/WHEEL +1 -1
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.6.3.dist-info}/entry_points.txt +1 -0
- sondera_harness-0.6.2.dist-info/METADATA +0 -323
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {sondera_harness-0.6.2.dist-info → sondera_harness-0.6.3.dist-info}/top_level.txt +0 -0
sondera/__init__.py
CHANGED
|
@@ -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",
|
sondera/adk/plugin.py
CHANGED
|
@@ -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",
|
sondera/harness/cedar/harness.py
CHANGED
|
@@ -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,
|
sondera/types.py
CHANGED
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
sondera/__init__.py,sha256=
|
|
1
|
+
sondera/__init__.py,sha256=0Z1R6ZkA5VSE_3ZCCgfeecm5nU6mFBdl4-18b8Nwt3Y,3558
|
|
2
2
|
sondera/__main__.py,sha256=MNgWvrV4g4Ot653Ngi2D4cAOyRIG19qHAWLzQecsMdg,66
|
|
3
3
|
sondera/cli.py,sha256=owchF-eA6kttYGTSsLl0B7XJMmn9O2n2LFjpfYQvznQ,494
|
|
4
4
|
sondera/exceptions.py,sha256=vtuToFc5tSlzAyVYvayyOatKBcoenisDA1RN2yk9aSI,3584
|
|
5
5
|
sondera/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
sondera/settings.py,sha256=bLB98vT75aXKh5ihYnCd0dTk1AdfUUaGuPkyzhcldE0,459
|
|
7
|
-
sondera/types.py,sha256=
|
|
7
|
+
sondera/types.py,sha256=t4TbVcieoPgGr9wje0jz0eFmXPmfOvdqsKds8aKgMDI,10428
|
|
8
8
|
sondera/adk/__init__.py,sha256=weoilnJyr8JNBv2HK6s3hhW-6rOBGBcNwwkKk1oHVFE,77
|
|
9
9
|
sondera/adk/analyze.py,sha256=IurwCWPZlNbMkIwi3TGWUu4k-w_VmKCkAnVFbfipbxY,7974
|
|
10
|
-
sondera/adk/plugin.py,sha256=
|
|
10
|
+
sondera/adk/plugin.py,sha256=ioxJCRaAC5Rt73h_Y6G3LgHSkz28K1hVi8nZGyCdAgc,13047
|
|
11
11
|
sondera/harness/__init__.py,sha256=gK0rFEyixD9X67pFyWKQpcq_oZ6pep6up8K0Y-zvXUc,221
|
|
12
12
|
sondera/harness/abc.py,sha256=hL77Rlzy1B-DjFexhskGm_9j5Sue-TaWdlSnjr-Al70,3548
|
|
13
13
|
sondera/harness/cedar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
sondera/harness/cedar/harness.py,sha256=
|
|
14
|
+
sondera/harness/cedar/harness.py,sha256=ZTUa1TuzNBLESkLMZZu_jAVOnqMOYLGgD1RbylqbhMk,15597
|
|
15
15
|
sondera/harness/cedar/schema.py,sha256=jDAGdLciK3fQ-7yKrqeFkU4YHQg-KwWvTi1leEuYVxo,7766
|
|
16
16
|
sondera/harness/sondera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
sondera/harness/sondera/_grpc.py,sha256=vLVJwJBtDwX-p8qZPfShpChKxdP3lSFoTV6QiL4dh7s,14708
|
|
@@ -70,9 +70,9 @@ sondera/tui/widgets/tool_card.py,sha256=3yNQcc_umcan4V1S5GiEKd7l4YA_atibwn3HF0n6
|
|
|
70
70
|
sondera/tui/widgets/violation_panel.py,sha256=fowe4KWb13NXLX0_RAxEPdRqYeyGzlImpRs4_L9y1zI,2933
|
|
71
71
|
sondera/tui/widgets/violations_list.py,sha256=86qICAsQOC6kjQLs64WxK7u59vEJ8kvfiToLVlzFyHM,2866
|
|
72
72
|
sondera/tui/widgets/violations_summary.py,sha256=e2LwqlB1aS8sZ2gEC5clk7siA16NSgePU1mpv8T1iTc,4473
|
|
73
|
-
sondera_harness-0.6.
|
|
74
|
-
sondera_harness-0.6.
|
|
75
|
-
sondera_harness-0.6.
|
|
76
|
-
sondera_harness-0.6.
|
|
77
|
-
sondera_harness-0.6.
|
|
78
|
-
sondera_harness-0.6.
|
|
73
|
+
sondera_harness-0.6.3.dist-info/licenses/LICENSE,sha256=DmSfauhgrslTxZOcDAmcYqsqsKBkMqVh3PYdjPghNbU,1070
|
|
74
|
+
sondera_harness-0.6.3.dist-info/METADATA,sha256=cWb8iOVxQLKyIRcNq8902znCu6oqPr8IRhU6Yvm64lg,6419
|
|
75
|
+
sondera_harness-0.6.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
76
|
+
sondera_harness-0.6.3.dist-info/entry_points.txt,sha256=5cLgW0-GzEwNnQjXIhGT21iFprQb1lftBaFJjC4IrgE,78
|
|
77
|
+
sondera_harness-0.6.3.dist-info/top_level.txt,sha256=BR0X8Gq9CCpwbQg5evpQfy5zwp9fTuGnlJhXSNqQ_hA,8
|
|
78
|
+
sondera_harness-0.6.3.dist-info/RECORD,,
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sondera-harness
|
|
3
|
-
Version: 0.6.2
|
|
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
|
-
# Sondera Harness SDK for Python
|
|
50
|
-
|
|
51
|
-
[](https://www.python.org/downloads/)
|
|
52
|
-
[](LICENSE)
|
|
53
|
-
|
|
54
|
-
>
|
|
55
|
-
> One step at a time. One action at a time. One trajectory at a time.
|
|
56
|
-
>
|
|
57
|
-
|
|
58
|
-
AI agents systems operate beyond traditional security boundaries, making autonomous decisions, calling tools, and accessing resources based on context that changes with every execution. Sondera SDK provides runtime governance for these agentic systems, answering not just "can this agent do X?" but "should it do X here, now, with this data?" Built for developers deploying agents through LangGraph, Google ADK, and Strands, Sondera enables real-time trajectory tracking, policy-as-code enforcement via Cedar, and behavioral adjudication so you can ship agents with confidence.
|
|
59
|
-
|
|
60
|
-
## Features
|
|
61
|
-
|
|
62
|
-
- **Managed harness-as-a-service** with the Sondera Harness for enterprise policy governance and guardrails
|
|
63
|
-
- **Local policy-as-code** using Cedar policy language in the Cedar Policy Harness
|
|
64
|
-
- **Real-time trajectory** observability, adjudication, and steering
|
|
65
|
-
- **Scaffold integrations** for LangGraph, Google ADK, and Strands
|
|
66
|
-
- **CLI and TUI** for monitoring agent behavior
|
|
67
|
-
|
|
68
|
-
## Installation
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
uv add sondera-harness
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Optional Dependencies
|
|
75
|
-
|
|
76
|
-
Install extras for specific framework integrations:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
# Google ADK support
|
|
80
|
-
uv add sondera-harness --extra adk
|
|
81
|
-
|
|
82
|
-
# LangGraph support
|
|
83
|
-
uv add sondera-harness --extra langgraph
|
|
84
|
-
|
|
85
|
-
# Strands support
|
|
86
|
-
uv add sondera-harness --extra strands
|
|
87
|
-
|
|
88
|
-
# All integrations
|
|
89
|
-
uv add sondera-harness --all-extras
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Quick Start
|
|
93
|
-
|
|
94
|
-
### Configuration
|
|
95
|
-
|
|
96
|
-
Set your API credentials via environment variables:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
export SONDERA_HARNESS_ENDPOINT="your-harness.sondera.ai:443"
|
|
100
|
-
export SONDERA_API_TOKEN="<YOUR_SONDERA_API_KEY>"
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Or create a `.env` file or `~/.sondera/env`:
|
|
104
|
-
|
|
105
|
-
```env
|
|
106
|
-
SONDERA_HARNESS_ENDPOINT=your-harness.sondera.ai:443
|
|
107
|
-
SONDERA_API_TOKEN=<YOUR_SONDERA_API_KEY>
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Scaffold Integrations
|
|
111
|
-
|
|
112
|
-
### LangGraph / LangChain
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
from langchain.agents import create_agent
|
|
116
|
-
from sondera.harness import SonderaRemoteHarness
|
|
117
|
-
from sondera.langgraph import SonderaHarnessMiddleware, Strategy, create_agent_from_langchain_tools
|
|
118
|
-
|
|
119
|
-
# Analyze your tools and create agent metadata
|
|
120
|
-
sondera_agent = create_agent_from_langchain_tools(
|
|
121
|
-
tools=my_tools,
|
|
122
|
-
agent_id="langchain-agent",
|
|
123
|
-
agent_name="My LangChain Agent",
|
|
124
|
-
agent_description="An agent that helps with tasks",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Create harness with agent
|
|
128
|
-
harness = SonderaRemoteHarness(agent=sondera_agent)
|
|
129
|
-
|
|
130
|
-
# Create middleware
|
|
131
|
-
middleware = SonderaHarnessMiddleware(
|
|
132
|
-
harness=harness,
|
|
133
|
-
strategy=Strategy.BLOCK, # or Strategy.STEER
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# Create agent with middleware
|
|
137
|
-
agent = create_agent(
|
|
138
|
-
model=my_model,
|
|
139
|
-
tools=my_tools,
|
|
140
|
-
middleware=[middleware],
|
|
141
|
-
)
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Google ADK
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
from google.adk.agents import Agent
|
|
148
|
-
from google.adk.runners import Runner
|
|
149
|
-
from sondera.harness import SonderaRemoteHarness
|
|
150
|
-
from sondera.adk import SonderaHarnessPlugin
|
|
151
|
-
|
|
152
|
-
# Create harness
|
|
153
|
-
harness = SonderaRemoteHarness(
|
|
154
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Create plugin
|
|
158
|
-
plugin = SonderaHarnessPlugin(harness=harness)
|
|
159
|
-
|
|
160
|
-
# Create agent
|
|
161
|
-
agent = Agent(
|
|
162
|
-
name="my-adk-agent",
|
|
163
|
-
model="gemini-2.0-flash",
|
|
164
|
-
instruction="Be helpful and safe",
|
|
165
|
-
tools=[...],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Create runner with plugin
|
|
169
|
-
runner = Runner(
|
|
170
|
-
agent=agent,
|
|
171
|
-
app_name="my-app",
|
|
172
|
-
plugins=[plugin],
|
|
173
|
-
)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Strands Agents
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
from strands import Agent
|
|
180
|
-
from sondera.harness import SonderaRemoteHarness
|
|
181
|
-
from sondera.strands import SonderaHarnessHook
|
|
182
|
-
|
|
183
|
-
# Create harness
|
|
184
|
-
harness = SonderaRemoteHarness(
|
|
185
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
# Create hook
|
|
189
|
-
hook = SonderaHarnessHook(harness=harness)
|
|
190
|
-
|
|
191
|
-
# Create agent with hook
|
|
192
|
-
agent = Agent(
|
|
193
|
-
system_prompt="You are a helpful assistant",
|
|
194
|
-
model="anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
195
|
-
hooks=[hook],
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
# Run agent (hooks fire automatically)
|
|
199
|
-
response = agent("What is 5 + 3?")
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Custom Scaffold
|
|
203
|
-
|
|
204
|
-
```python
|
|
205
|
-
from sondera import SonderaRemoteHarness, Agent, PromptContent, Role, Stage
|
|
206
|
-
|
|
207
|
-
# Create a harness instance
|
|
208
|
-
harness = SonderaRemoteHarness(
|
|
209
|
-
sondera_harness_endpoint="localhost:50051",
|
|
210
|
-
sondera_api_key="<YOUR_SONDERA_API_KEY>",
|
|
211
|
-
sondera_harness_client_secure=True, # Enable TLS for production
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
# Define your agent
|
|
215
|
-
agent = Agent(
|
|
216
|
-
id="my-agent",
|
|
217
|
-
provider_id="custom",
|
|
218
|
-
name="My Assistant",
|
|
219
|
-
description="A helpful AI assistant",
|
|
220
|
-
instruction="Be helpful, accurate, and safe",
|
|
221
|
-
tools=[],
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Initialize a trajectory
|
|
225
|
-
await harness.initialize(agent=agent)
|
|
226
|
-
|
|
227
|
-
# Adjudicate user input
|
|
228
|
-
adjudication = await harness.adjudicate(
|
|
229
|
-
Stage.PRE_MODEL,
|
|
230
|
-
Role.USER,
|
|
231
|
-
PromptContent(text="Hello, can you help me?"),
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
if adjudication.is_allowed:
|
|
235
|
-
# Proceed with agent logic
|
|
236
|
-
pass
|
|
237
|
-
elif adjudication.is_denied:
|
|
238
|
-
print(f"Request blocked: {adjudication.reason}")
|
|
239
|
-
|
|
240
|
-
# Finalize the trajectory
|
|
241
|
-
await harness.finalize()
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Remote and Local Harnesses
|
|
245
|
-
|
|
246
|
-
### Cedar Policy Harness (Local Only)
|
|
247
|
-
|
|
248
|
-
For a local harness deployment, you can use the `CedarPolicyHarness` to evaluate Cedar policies:
|
|
249
|
-
|
|
250
|
-
```python
|
|
251
|
-
from sondera.harness import CedarPolicyHarness
|
|
252
|
-
from sondera import Agent
|
|
253
|
-
|
|
254
|
-
# Define Cedar policies
|
|
255
|
-
policies = '''
|
|
256
|
-
@id("forbid-dangerous-bash")
|
|
257
|
-
forbid(
|
|
258
|
-
principal,
|
|
259
|
-
action == Coding_Agent::Action::"Bash",
|
|
260
|
-
resource
|
|
261
|
-
)
|
|
262
|
-
when {
|
|
263
|
-
context has parameters &&
|
|
264
|
-
(context.parameters.command like "*rm -rf /*" ||
|
|
265
|
-
context.parameters.command like "*mkfs*" ||
|
|
266
|
-
context.parameters.command like "*dd if=/dev/zero*" ||
|
|
267
|
-
context.parameters.command like "*> /dev/sda*")
|
|
268
|
-
};
|
|
269
|
-
'''
|
|
270
|
-
|
|
271
|
-
# Create local policy engine
|
|
272
|
-
harness = CedarPolicyHarness(
|
|
273
|
-
policy_set=policies,
|
|
274
|
-
agent=my_agent,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
await harness.initialize()
|
|
278
|
-
# Use same adjudication API as RemoteHarness
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## CLI & TUI
|
|
282
|
-
|
|
283
|
-
Launch the Sondera TUI for monitoring (note, requires a Sondera account and API key):
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
sondera
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The TUI provides:
|
|
290
|
-
- Real-time agent and trajectory overview
|
|
291
|
-
- Adjudication history and policy violations
|
|
292
|
-
- Agent details and tool inspection
|
|
293
|
-
|
|
294
|
-
## Examples
|
|
295
|
-
|
|
296
|
-
See the [examples/](examples/) directory for complete demos:
|
|
297
|
-
|
|
298
|
-
- **LangGraph**: Investment chatbot with policy enforcement
|
|
299
|
-
- **ADK**: Payment and healthcare agents
|
|
300
|
-
- **Strands**: Various agent implementations
|
|
301
|
-
|
|
302
|
-
## Environment Variables
|
|
303
|
-
|
|
304
|
-
| Variable | Description | Default |
|
|
305
|
-
|----------|-------------|---------|
|
|
306
|
-
| `SONDERA_HARNESS_ENDPOINT` | Harness service endpoint | `localhost:50051` |
|
|
307
|
-
| `SONDERA_API_TOKEN` | JWT authentication token | Required for remote |
|
|
308
|
-
|
|
309
|
-
## Requirements
|
|
310
|
-
|
|
311
|
-
- Python 3.12 or higher (up to 3.14)
|
|
312
|
-
|
|
313
|
-
## Security
|
|
314
|
-
|
|
315
|
-
See [SECURITY.md](SECURITY.md) for security best practices and vulnerability reporting.
|
|
316
|
-
|
|
317
|
-
## Contributing
|
|
318
|
-
|
|
319
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
320
|
-
|
|
321
|
-
## License
|
|
322
|
-
|
|
323
|
-
MIT - see [LICENSE](LICENSE) for details.
|
|
File without changes
|
|
File without changes
|