Skip to main content

Vendor-neutral control plane for AI agents — governance, audit, and (soon) runtime enforcement.

Project description

Veragent Python SDK

The client library that makes Veragent a vendor-neutral control plane instead of a single bespoke integration. Instrument any agent — custom, LangChain, LangGraph, OpenAI Agents SDK, MCP, and CrewAI — in a couple of lines, and have its activity captured in one place under your policies.

The core SDK is dependency-free (standard library only). Framework adapters pull in just what they need.

Install

pip install veragent                 # core
pip install "veragent[langchain]"    # + LangChain adapter
pip install "veragent[openai-agents]" # + OpenAI Agents SDK adapter
pip install "veragent[mcp]"           # + MCP interceptor
pip install "veragent[crewai]"        # + CrewAI adapter

Quick start

from veragent import Veragent

va = Veragent(agent="my-agent")      # reads VERAGENT_API_KEY from the environment
va.track("task completed", severity="info")

Custom agents — decorator + run correlation

@va.track_action("broker.place_order")
def place_order(symbol, size): ...

with va.run("nightly-cycle") as run:
    run.track("fetched markets", severity="info")
    place_order("AAPL", 10)          # captured automatically, errors included

LangChain / LangGraph — no changes to your agent

from veragent.integrations.langchain import VeragentCallback

chain.invoke(payload, config={"callbacks": [VeragentCallback(va)]})

Every LLM call, tool call, and error in the run is captured and normalized. Tool calls — the actions with side effects — are tracked as event_type="tool_call", which is the surface that matters for governance.

OpenAI Agents SDK — no changes to your agent

from veragent.integrations.openai_agents import instrument

instrument(va)   # register once, before running any agents
# ... build Agents and call Runner.run() exactly as before ...

instrument(va) adds a tracing processor, so every run, generation, tool/function call, handoff, and guardrail is captured. It's additive — your existing OpenAI tracing keeps working. Function (tool) calls are tracked as event_type="tool_call".

MCP — capture (and optionally enforce) tool calls

from veragent.integrations.mcp import instrument_mcp

instrument_mcp(session, va)                 # capture every tool call on this session
# instrument_mcp(session, va, enforce=True) # also gate each call via /api/authorize

MCP is the framework-agnostic tool layer, so instrumenting session.call_tool captures the governable surface no matter what drives the session. With enforce=True, each call is authorized first and a denied call is blocked before the real tool runs (it returns an isError result). Enforcement stays inert until the client's enforcement_enabled is on, so it's safe to wire in now.

CrewAI — no changes to your crew

from veragent.integrations.crewai import instrument

instrument(va)   # register once, before kicking off any crew
# ... build your Crew and call crew.kickoff() exactly as before ...

instrument(va) registers a listener on CrewAI's event bus, so every crew, task, agent, tool, and LLM event is captured. Tool calls are tracked as event_type="tool_call".

Authorize — ask permission before acting

va.authorize(...) is the pre-action decision point. It calls the live /api/authorize endpoint with the same agent name + API key you configured the client with — you never re-pass credentials. It stays inert (reporting-only, allowed=True) until you turn enforcement on, so it's safe to wire in first:

va = Veragent(agent="crypto-paper-trader", enforcement_enabled=True)

Blocking (the simple case)

decision = va.authorize(
    "polymarket.place_order",
    context={"market": "BTC-100k", "size": 250},
)

if decision.allowed:
    place_order(...)            # ✅ authorized
else:
    # MUST handle the not-allowed branch — these mean different things:
    if decision.status == "denied":
        log.warning("blocked by policy: %s", decision.reason)
    elif decision.status == "timed_out":
        log.warning("no human answered in time; server fail mode applied")
    elif decision.status == "error":
        log.error("couldn't reach Veragent: %s", decision.reason)
    skip_or_rollback()

If the action is auto-decided, the answer comes back on the first call with zero added latency. If it escalates to a human, authorize() blocks and polls until the decision is terminal or timeout_seconds (default 300, server clamps 10–3600) elapses.

Async (high-throughput loops)

Don't block a hot loop on a human. Fire the request, do other work, poll later:

decision = va.authorize("refund.issue", context={"order": "A-1001"}, wait="async")
# returns immediately — terminal OR status="pending"

# ...later, poll yourself:
decision = va.get_decision(decision.decision_id)
if decision.status == "pending":
    ...        # still waiting on a human; check again later
elif decision.allowed:
    issue_refund(...)

The decision object

Decision(
    allowed: bool,         # the one thing you must check
    status: str,           # "allowed" | "denied" | "timed_out" | "error" | "pending"
    decision_id: str,
    reason: str,
    decided_by: str | None,
    resolved_at: str | None,
)

if decision: works too (__bool__ returns allowed).

Treat authorize() as fallible. It is a network call. A client-side inability to reach Veragent returns status="error", allowed=False (fail-safe — never silently "allowed"), which is not the same as a policy denied. Distinguish denied (a real no) from timed_out (nobody answered → the server's fail mode decided) from error (couldn't get an answer at all). Prefer wait="async" for high-throughput loops, and keep privileged/irreversible actions late and rollback-able so a denial or error is cheap to honor.

Acting for a human: on_behalf_of

context["on_behalf_of"] is a reserved-by-convention context key naming the human principal the agent is acting for — "this agent acted on behalf of user X". No SDK machinery is involved: context passes through verbatim, and governance rules can condition on context.on_behalf_of today (any operator, e.g. escalate when an agent acts for no named principal).

decision = va.authorize(
    "Refund €420 to customer 8821",
    permission="PAYMENT",
    context={"on_behalf_of": "sarah.chen@customer-corp.com", "amount": 420},
)

Two rules of the road:

  • Opaque and yours. An email, an IdP subject id, any stable string — pick one form per organisation and keep it stable.
  • Attested by the caller, never verified by Veragent. The platform cannot resolve your identity provider; this is attribution evidence on the decision record, not authentication.

Emitting the same key in track() payloads (inputs={"on_behalf_of": …} → stored as metadata.on_behalf_of) is encouraged for consistency — event-side rules read the same name via the platform's context↔metadata alias.

Failure semantics

What happens when things go wrong is a first-class part of the contract — read this before you enforce. There are three distinct layers, and they fail differently on purpose.

1. Enforcement is OFF by default — this is the deliberate fail-open default. A client constructed without enforcement_enabled=True never makes an authorization network call at all: authorize() returns Decision(allowed=True, status="allowed") immediately (reporting-only), and the adapters (enforce defaults to False) run every tool call. Out of the box, nothing is ever blocked. This lets you wire authorize() through your codebase safely before you are ready to enforce. Until you opt in, there is no enforcement guarantee — by design.

2. With enforcement ON, an unreachable Veragent fails safe — your action does not get a "yes". When enforcement_enabled=True and the authorize() call cannot reach Veragent (network failure or client-side timeout), the SDK returns Decision(allowed=False, status="error") — it never fabricates an approval. If you honor decision.allowed (and the enforce=True adapters do — they block the call), an outage means the guarded action does not proceed. At the SDK boundary an outage is therefore fail-closed: the agent never proceeds thinking it was authorized. Distinguish the three not-allowed statuses: denied (a real policy/human no), timed_out (an escalation nobody answered — see layer 3), and error (Veragent could not be reached at all).

3. Escalation timeouts are decided server-side by the matched policy's fail mode. When a policy escalates an action to a human and no one answers before the deadline, the outcome is governed by that policy's per-rule fail mode, configured in the dashboard — not by the SDK:

  • fail_closed — on timeout the request resolves denied. Use it for high-stakes, low-frequency actions (a wire transfer should not go through because an approver was at lunch).
  • fail_open — on timeout the request resolves allowed. Use it for high-frequency actions where blocking the agent does more harm than the occasional un-reviewed pass.

The SDK reports this back as status="timed_out" with allowed reflecting the policy's fail mode.

The enforce-mode default stays fail-open at the enforcement toggle (layer 1) and fail-safe at the network boundary (layer 2). Keep privileged or irreversible actions late and rollback-able so that honoring a denied or an error is cheap. Unlike track() (which drops failures silently so it can never slow your agent), authorize() failures are always legible — surfaced in the returned Decision, never swallowed.

Design guarantees

  • Safe. track() only enqueues; all network I/O is on a background worker. Instrumentation never blocks, slows, or crashes the host agent, and never raises into your code path. If Veragent is unreachable, events are dropped with a log line — your agent keeps running.
  • Redaction-first. Inputs and outputs pass through a redactor before they leave the process — sensitive keys (passwords, tokens, emails, IBANs, card numbers, …) are stripped and long strings truncated. On by default. Pass redact=False to disable or supply your own callable. This is what keeps customer data out of governance telemetry.
  • Enforcement-ready. va.authorize(action, context=...) is the pre-action decision point, calling the live /api/authorize endpoint. It is safe to wire in today — while enforcement_enabled is off it returns allowed=True (reporting-only); turn it on to get real allow/deny verdicts. Unlike track(), authorize failures are legible, not silent: if Veragent can't be reached you get status="error", allowed=False so the agent never proceeds thinking it was authorized. See Authorize.

Event envelope

Backward-compatible with the current ingest contract. agent / action / severity stay at the top level (read by the server today); the structured envelope rides in metadata:

{
  "agent": "my-agent",
  "action": "tool call: refund",
  "severity": "info",
  "metadata": {
    "event_type": "tool_call",
    "tool": "refund",
    "inputs": { "order_id": "A-1001", "amount": 49.0 },
    "run_id": "…", "span_id": "…",
    "ts": "2026-…", "sdk": "veragent-python/<package version>"
  }
}

Metadata is stored and governable. The metadata object is persisted verbatim on the platform's audit record, and it is the event-side rule vocabulary: dashboard rules can condition on metadata.<key>, and event-metered agent budgets sum numeric keys over sliding windows. Two names are the documented convention — metadata.cost (the step's monetary cost, your unit) and metadata.tokens (LLM tokens for the step). Emit plain numbers, per-event (not running totals); any other [A-Za-z0-9_-]+ key works the same way.

Adapter roadmap

Adapter Status
Custom agents (decorator, run() context, track()) ✅ shipped
LangChain / LangGraph (VeragentCallback) ✅ shipped
OpenAI Agents SDK (tracing processor) ✅ shipped
CrewAI (event-bus listener) ✅ shipped
MCP interceptor (capture + enforce) ✅ shipped
TypeScript / Node SDK (parity) ✅ shipped — veragent on npm
OpenTelemetry / OpenInference exporter (zero-code for OTel shops) ▢ next

Versioning & stability

This SDK is pre-1.0 (0.x), and here is exactly what that means. Within 0.x we keep the surfaces you build against stable: the Decision shape (allowed / status / decision_id / reason and the five status values), the constructor arguments documented above, the adapter entry points, and the wire envelope. Changes are additive — new arguments, new fields, new adapters. If we ever have to break one of those, the minor version jumps with a loud changelog entry; nothing breaks in a patch release.

The platform side of the contract is versioned independently and published: the failure semantics above, the decision statuses, and the server's own stability promises are stated at veragent.io/docs/api, and the machine-readable Management API contract is served at /api/v1/openapi.yaml. (The Management API is the admin plane — this SDK speaks the agent plane: /api/ingest-event and /api/authorize — but the docs page's frozen-semantics inventory governs both.)

The PyPI and npm packages version independentlyveragent on PyPI and veragent on npm move at their own pace and their version numbers are not meant to match. A lower number on one registry is not staleness; both speak the same wire contract, and each README states its own guarantees.

Changelog

0.4.0 (2026-07-22)

  • Removed the fail_closed constructor argument. It was decorative from the day it shipped — assigned and never read; no fail-open path ever existed behind it. Behavior is unchanged: with enforcement on, an unreachable Veragent has always returned Decision(allowed=False, status="error"). If you passed fail_closed=..., delete the argument — it now raises TypeError. (This is the loud minor-version break the stability statement above describes.)
  • New documentation: the three-layer Failure semantics contract, on_behalf_of attribution, this Versioning & stability statement, and a corrected event-envelope section — metadata is persisted and governable (the old "sent but not stored" note described a pre-launch platform and had long been false).
  • The wire tag sdk: "veragent-python/<version>" now tracks the package version — it had been stuck at 0.2.0 since 0.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

veragent-0.4.0.tar.gz (36.2 kB view details)

Uploaded Source

Built Distribution

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

veragent-0.4.0-py3-none-any.whl (30.1 kB view details)

Uploaded Python 3

File details

Details for the file veragent-0.4.0.tar.gz.

File metadata

  • Download URL: veragent-0.4.0.tar.gz
  • Upload date:
  • Size: 36.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for veragent-0.4.0.tar.gz
Algorithm Hash digest
SHA256 1060bea678786d3b469fcc39d177b8d589e1834a863fe29f69d3060ba3c0a436
MD5 d10561c33ffb537f87a215675c786b6a
BLAKE2b-256 d348140a40d392134e2dc7a4b7f7e6bd50ed1ecbf65d948fbc7f7ee7521752d7

See more details on using hashes here.

File details

Details for the file veragent-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: veragent-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 30.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for veragent-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 72f522285f2cdbd7428d6a65dfa50cbc1651b5156fa7d87e5fafdfed5a9d5f28
MD5 8f8fbdfe1d7f61ed047387bb2a2f8214
BLAKE2b-256 35e7b44c95ceb826283b1d7ed27151694e366a6e794e391b400fa188e910aae6

See more details on using hashes here.

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