Metadata-Version: 2.4
Name: veragent
Version: 0.4.0
Summary: Vendor-neutral control plane for AI agents — governance, audit, and (soon) runtime enforcement.
License-Expression: Apache-2.0
Project-URL: Homepage, https://www.veragent.io
Project-URL: Documentation, https://www.veragent.io/docs
Project-URL: API stability, https://www.veragent.io/docs/api
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.2; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.1; extra == "openai-agents"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: crewai
Requires-Dist: crewai>=1.0; extra == "crewai"
Provides-Extra: dev
Requires-Dist: langchain-core>=0.2; extra == "dev"
Requires-Dist: openai-agents>=0.1; extra == "dev"
Requires-Dist: mcp>=1.0; extra == "dev"
Requires-Dist: crewai>=1.0; extra == "dev"
Dynamic: license-file

# 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

```bash
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

```python
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

```python
@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

```python
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

```python
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

```python
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

```python
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:

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

### Blocking (the simple case)

```python
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:

```python
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

```python
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*).

```python
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](#authorize--ask-permission-before-acting).

## 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`:

```json
{
  "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](https://www.npmjs.com/package/veragent) |
| 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](https://www.veragent.io/docs/api)**, and the machine-readable Management API contract is served at [`/api/v1/openapi.yaml`](https://www.veragent.io/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 independently** — `veragent` 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.
