Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clavenar_agent_sdk-1.5.0.tar.gz (63.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

clavenar_agent_sdk-1.5.0-py3-none-any.whl (41.5 kB view details)

Uploaded Python 3

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

Hashes for clavenar_agent_sdk-1.5.0.tar.gz
Algorithm Hash digest
SHA256 f8bfc8d103f946e27f30fbaee4649e4108dff3a1c45c918e330f0ba24d80188d
MD5 d6197d693b7b9621221a88b51f06e717
BLAKE2b-256 a21bfc7630302c3c1d38b0f60835be79790b28b13415f431e02d709b25652665

See more details on using hashes here.

Provenance

The following attestation bundles were made for clavenar_agent_sdk-1.5.0.tar.gz:

Publisher: release.yml on clavenar/clavenar-python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file clavenar_agent_sdk-1.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for clavenar_agent_sdk-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e50e3b1f54c41466cf10fa1e6b52b151f58d12385b0f5c779692d1d86748c93
MD5 51fb9d6f759495deb53d51667d3dd0e4
BLAKE2b-256 e25442cf1d168592a4f0ad873e1c5b665d39117ff22bec5deb7191ced4cff3b7

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page