Wrap your Anthropic / OpenAI Python client. Every tool call is inspected by Clavenar before it runs.
Project description
clavenar-agent-sdk (Python)
Wrap your Anthropic / OpenAI Python client — async or sync. Every tool call the model emits is inspected by Clavenar before the agent loop can run it.
Sequence diagrams for the five primary paths — clavenar_wrap boot
with sync/async fork, async non-streaming inspection, async
streaming choice-end gating, ClavenarPending.resolve poll loop, and
the standalone OpenAI Realtime helper — plus a request
decision-tree flowchart, live in
docs/SEQUENCES.md.
import asyncio
from anthropic import AsyncAnthropic
from clavenar_agent_sdk import clavenar_wrap, ClavenarDenied, ClavenarOptions
async def main() -> None:
client = clavenar_wrap(
AsyncAnthropic(),
ClavenarOptions(endpoint="http://localhost:8080", mode="enforce"),
)
try:
result = await client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
tools=[...],
messages=[{"role": "user", "content": "list my files"}],
)
except ClavenarDenied as e:
print(f"clavenar denied {e.tool_name}: {e.reasons}")
asyncio.run(main())
OpenAI works the same way:
from openai import AsyncOpenAI
from clavenar_agent_sdk import clavenar_wrap, ClavenarOptions
client = clavenar_wrap(
AsyncOpenAI(),
ClavenarOptions(endpoint="http://localhost:8080"),
)
completion = await client.chat.completions.create(
model="gpt-5",
tools=[...],
messages=[...],
)
Sync clients
anthropic.Anthropic and openai.OpenAI (non-async) are wrapped
exactly the same way — clavenar_wrap detects sync vs. async by
inspecting the underlying create method:
from anthropic import Anthropic
from clavenar_agent_sdk import clavenar_wrap, ClavenarOptions
client = clavenar_wrap(
Anthropic(),
ClavenarOptions(endpoint="http://localhost:8080"),
)
result = client.messages.create(model="claude-opus-4-7", ...)
Sync clients use httpx.Client under the hood. Both sync and async wrappers
select the side-effect-free clavenar.decision/v1 contract with a UUID
allocated before the first attempt; a multi-tool turn is one ordered atomic
decision. Proxy 0.5.0 and Lite 0.9.0 reject unselected tool calls with HTTP 426;
upgrade this SDK before the gateway by following
https://clavenar.com/docs/sdk-migration/. Callbacks (on_verdict, on_policy_error) must be sync
when wrapping a sync client.
Streaming
stream=True is intercepted: each event/chunk passes through in
order, but the closing event (Anthropic content_block_stop, OpenAI
finish_reason="tool_calls") is held until clavenar returns a verdict.
A denied tool raises mid-iteration before partner code can act on
it.
async with client.messages.create(stream=True, ...) as stream:
async for event in stream:
...
# ClavenarDenied raised inside the async-for if a tool_use was blocked.
Both AsyncAnthropic + AsyncOpenAI streams and their sync
counterparts (Anthropic, OpenAI) are supported.
The provider SDKs' .stream() convenience helpers
(messages.stream(), chat.completions.stream()) are blocked by
the wrapper: their rich event interfaces can't be wrapped faithfully,
so tool calls made through them would bypass inspection entirely.
Calling one raises ClavenarConfigError pointing at
create(stream=True); set allow_uninspected_stream=True only if you
explicitly accept uninspected streaming.
Pending → resolve
When clavenar parks a tool call for human review, ClavenarPending is
raised. Catch it and await resolve() to block until an operator
decides:
try:
result = await client.messages.create(...)
except ClavenarPending as p:
print(f"awaiting approval: {p.review_reasons}")
await p.resolve(poll_interval_s=2.0, timeout_s=600.0)
# Returns on approve; raises ClavenarDenied on deny.
Transient transport errors (5xx, network blips) are swallowed between polls. Terminal errors (404, 401) re-raise immediately.
Debugging a denied tool call
ClavenarDenied carries reasons, layer, and correlation_id. To see
which detector fired, run the gateway with
CLAVENAR_PROXY_VERBOSE_VERDICTS=true (Lite: --verbose-verdicts) — the
deny then carries a per-detector detail breakdown, exposed as
err.detail and rendered to stderr when you set dev_mode=True:
client = clavenar_wrap(
anthropic,
ClavenarOptions(
endpoint="https://clavenar.internal",
dev_mode=True, # dev/staging only — detailed denials are an attacker oracle
),
)
# On a deny, the SDK prints a panel to stderr:
# ━━ clavenar denied: send_email ━━
# layer=brain intent=Exfiltration correlation=abc-123
# detectors:
# persona_drift 0.12
# injection 0.91 ⚠ flagged
# degraded: injection
Programmatic access (no dev_mode needed):
try:
await client.messages.create(...)
except ClavenarDenied as e:
if e.detail:
fired = [d for d in e.detail["detectors"] if d.get("flagged") or d["score"] >= 0.5]
print("fired detectors:", fired)
detail is None unless the gateway opts in; without it the panel prints
a hint to enable verbose verdicts. render_deny_panel(err) returns the
string if you want it without writing to stderr.
Retries
Network errors and 5xx responses retry up to max_attempts with
jittered exponential backoff. 200, 403, and other 4xx never retry.
Defaults mirror the TS SDK at 1.1.0:
from clavenar_agent_sdk import ClavenarOptions, ClavenarRetryOptions
opts = ClavenarOptions(
endpoint="...",
retry=ClavenarRetryOptions(max_attempts=3, base_delay_s=0.1),
)
Set max_attempts=1 to disable retries.
Modes
| Mode | Deny | Transport failure |
|---|---|---|
enforce (default) |
raises ClavenarDenied |
raises ClavenarTransportError after retries |
observe |
passes through; on_verdict fires |
passes through; on_policy_error fires |
Observe is the rollout knob — surface what clavenar would decide without breaking the agent. Flip to enforce per-call once verdicts are trusted.
Install
pip install clavenar-agent-sdk==1.5.0
Python 3.10+. Runtime dep is httpx only; the anthropic and
openai packages are NOT imported by clavenar-agent-sdk — bring your own.
Configuration
| Field | Type | Default | Notes |
|---|---|---|---|
endpoint |
str |
— | clavenar-lite base URL, e.g. http://localhost:8080 |
token |
str | None |
None |
Shared bearer (CLAVENAR_LITE_TOKEN) |
mode |
"enforce" | "observe" |
"enforce" |
Mirror of server-side CLAVENAR_MODE |
timeout_s |
float |
10.0 |
Per-request timeout |
on_verdict |
callable | None |
None |
Fired per inspected tool call |
on_policy_error |
callable | None |
None |
Fired per transport failure in observe mode |
extra_headers |
dict[str, str] |
{} |
Forwarded on every inspect (X-Clavenar-Demo-Prefix, proxy auth, …) |
retry |
ClavenarRetryOptions |
(3, 0.1) |
Jittered exponential backoff for 5xx + network errors |
Wire contract
The HTTP shape this SDK speaks against the inspect endpoint
(POST /mcp, the verdict envelope, the pending / resolve
contract, and the X-Clavenar-* header set) is documented in the
workspace's source of truth:
clavenar-specs/TECH_SPEC.md.
This SDK is a faithful client of that contract — if you observe a
divergence, file the bug against the spec first.
The TypeScript sibling at
clavenar-typescript-sdk
implements the same wire contract with parity guarantees.
License
Apache-2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clavenar_agent_sdk-1.5.0.tar.gz.
File metadata
- Download URL: clavenar_agent_sdk-1.5.0.tar.gz
- Upload date:
- Size: 63.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8bfc8d103f946e27f30fbaee4649e4108dff3a1c45c918e330f0ba24d80188d
|
|
| MD5 |
d6197d693b7b9621221a88b51f06e717
|
|
| BLAKE2b-256 |
a21bfc7630302c3c1d38b0f60835be79790b28b13415f431e02d709b25652665
|
Provenance
The following attestation bundles were made for clavenar_agent_sdk-1.5.0.tar.gz:
Publisher:
release.yml on clavenar/clavenar-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clavenar_agent_sdk-1.5.0.tar.gz -
Subject digest:
f8bfc8d103f946e27f30fbaee4649e4108dff3a1c45c918e330f0ba24d80188d - Sigstore transparency entry: 2262582008
- Sigstore integration time:
-
Permalink:
clavenar/clavenar-python-sdk@cd16eab83d38f184ac7136888c23c6410aecde78 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/clavenar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd16eab83d38f184ac7136888c23c6410aecde78 -
Trigger Event:
repository_dispatch
-
Statement type:
File details
Details for the file clavenar_agent_sdk-1.5.0-py3-none-any.whl.
File metadata
- Download URL: clavenar_agent_sdk-1.5.0-py3-none-any.whl
- Upload date:
- Size: 41.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e50e3b1f54c41466cf10fa1e6b52b151f58d12385b0f5c779692d1d86748c93
|
|
| MD5 |
51fb9d6f759495deb53d51667d3dd0e4
|
|
| BLAKE2b-256 |
e25442cf1d168592a4f0ad873e1c5b665d39117ff22bec5deb7191ced4cff3b7
|
Provenance
The following attestation bundles were made for clavenar_agent_sdk-1.5.0-py3-none-any.whl:
Publisher:
release.yml on clavenar/clavenar-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clavenar_agent_sdk-1.5.0-py3-none-any.whl -
Subject digest:
9e50e3b1f54c41466cf10fa1e6b52b151f58d12385b0f5c779692d1d86748c93 - Sigstore transparency entry: 2262582402
- Sigstore integration time:
-
Permalink:
clavenar/clavenar-python-sdk@cd16eab83d38f184ac7136888c23c6410aecde78 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/clavenar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd16eab83d38f184ac7136888c23c6410aecde78 -
Trigger Event:
repository_dispatch
-
Statement type: