---
title: Python SDK
description: The complete veefore-py surface — guard, protect, the client, dataclasses and typed exceptions.
label: Python SDK
---

```bash
pip install veefore-py
```

The package is `veefore-py`; the module you import is `veefore`. Python 3.9+.
One dependency: `cryptography`, for Ed25519 signing.

Running `veefore` in your project installs this package with your project's
tool (pip, uv or poetry), writes its configuration and adds `@guard` to your
tools. See [Automatic setup](/build/automatic-setup).

:::security A convenience layer, never a security boundary
Every protected action is decided server-side by the gateway on every call.
Nothing in this package can grant an agent authority it does not already have,
and nothing here caches a permission.
:::

The Python and TypeScript SDKs produce byte-identical canonical payloads and
signatures.

## `@guard(tool_key, **options)`

The way most agents integrate. Decorate the function that performs a tool, and
every call is decided by the gateway before the function runs.

```python
from veefore import guard
from langchain_core.tools import tool

@tool
@guard("refund_customer")
def refund_customer(order_id: str, amount: int) -> str:
    """Refund a customer order."""
    return payments.refund(order_id, amount)
```

Put `@guard` directly above `def`, below the framework's decorator. It uses
`functools.wraps`, so `@tool`, `@function_tool` and `@mcp.tool()` still see the
original name, signature, type hints and docstring. `async def` functions work
too; the gateway call runs off the event loop.

| Decision | What `@guard` does |
| --- | --- |
| Permitted, `CLIENT` tool | Runs the function and returns its result |
| Permitted, brokered `HTTP` tool | Returns the gateway's result **without** running the function |
| Refused | Raises `VeeforeBlockedError` |
| Needs a human | Raises `VeeforeApprovalRequiredError`, or waits with `wait_for_approval` |

The call's arguments, by name, are sent as the policy `parameters`.

:::params

:::param capability | str | optional
Defaults to `tool.<tool_key>`, which `veefore init` registers.
:::

:::param wait_for_approval | bool or float | optional
Wait for a human instead of raising. `True` waits up to 900 seconds; a number
sets the timeout in seconds.
:::

:::param describe | Callable[..., dict] | optional
Returns extra policy inputs for a call, such as `amount`, `currency`,
`destination` or `purpose`.
:::

:::param client | VeeforeClient | optional
Use this client instead of the shared one built from the environment.
:::

:::

Configuration comes from the `VEEFORE_*` environment variables. If they are not
set, `@guard` reads the block `veefore init` writes to `.env.local`; values
already in the environment always win.

## `protect(agent, **options)`

For code that calls Veefore directly rather than through a tool function.

```python
from veefore import protect

agent = protect(my_agent, name="finance-agent")
```

Returns a `ProtectedAgent` wrapping your object. Attribute access falls through
to the original; `veefore`, `act`, `evaluate`, `simulate` and `handshake` are
added.

:::params

:::param name | str | optional
Display metadata. Never used for authorization.
:::

:::param gateway_url | str | required
From `VEEFORE_GATEWAY_URL` if not passed.
:::

:::param agent_id | str | required
From `VEEFORE_AGENT_ID`.
:::

:::param private_key | str | required
Base64 PKCS#8 Ed25519. From `VEEFORE_AGENT_PRIVATE_KEY`.
:::

:::param key_id | str | required
From `VEEFORE_AGENT_KEY_ID`.
:::

:::param audience | str | optional
Must match the gateway's published audience exactly. Defaults to `gateway_url`.
:::

:::param declared_tools | list[str] | optional
Reported at handshake, so an unauthorized tool is a startup warning.
:::

:::param eager_handshake | bool | optional
Opens the session immediately. Defaults to `False`.
:::

:::param request_timeout_seconds | float | optional
Per-request timeout. Note: **seconds**, where the TypeScript SDK uses
milliseconds.
:::

:::param max_retries | int | optional
Transport errors only. Decisions are never retried.
:::

:::param on_log | Callable | optional
Structured diagnostics. Never receives secrets.
:::

:::

Missing required configuration raises `VeeforeConfigurationError` immediately.

## `create_client()` and `config_from_env()`

```python
from veefore import config_from_env, create_client

client = create_client(name="worker")
partial = config_from_env()
```

Use `create_client` when there is no agent object to wrap.

## `VeeforeClient`

```python
info = agent.handshake()
info.mode                # 'OBSERVE' | 'SIMULATE' | 'ENFORCE'
info.capabilities
info.authorized_tools
info.passport_version
info.expires_at

agent.veefore.authenticate()
agent.veefore.info       # last HandshakeResult, or None
```

| Method | Behaviour |
| --- | --- |
| `act(request)` | Performs the action. Raises when refused |
| `evaluate(request)` | Returns the decision as data |
| `simulate(request)` | `evaluate` with `dry_run=True` |
| `act_with_approval(request, *, timeout_seconds=900.0)` | Waits for a human, then resubmits |
| `approval_status(approval_id)` | Current status |
| `wait_for_approval(approval_id, ...)` | Polls, honouring `poll_after_ms` |
| `mcp_call(...)` | Invoke an authorized MCP tool |
| `a2a_invoke(...)` | Invoke a verified A2A peer |

## `ActionRequest`

A frozen dataclass.

```python
from veefore import ActionRequest

request = ActionRequest(
    tool="refund_customer",            # required
    action="create",
    capability="refund.create",
    parameters={"orderId": "ord_8812", "reason": "damaged on arrival"},
    resource={"key": "ord_8812", "type": "order"},
    amount=250_00,                     # minor units; a policy input only
    currency="INR",
    destination="payments.internal.example.com",
    purpose="Customer reported damage; agent verified photo evidence.",
    idempotency_key="refund-ord_8812-attempt-1",
    approval_id=None,
    dry_run=None,
    metadata=None,
)
```

Fields use `snake_case`; the SDK converts to the wire format. Fill in `purpose`
— it is what an approver reads.

## `ActionDecision`

```python
decision = agent.act(request)

decision.allowed        # bool — branch on this
decision.decision       # 'ALLOW' | 'BLOCK' | 'REQUIRE_APPROVAL' | …
decision.outcome
decision.mode
decision.result         # Optional[dict]
decision.reason
decision.explanation    # dict
decision.approval       # Optional[dict]
decision.risk           # Optional[dict]
decision.shadow         # Optional[dict]
decision.limits         # Optional[dict]
decision.latency_ms
decision.stages         # list[StageResult]
decision.action_id
decision.request_id
decision.trace_id
```

:::note Nested fields are plain dicts
`approval`, `risk`, `shadow`, `limits` and `explanation` are `dict`, not
dataclasses, because their shapes are extended server-side over time and a
frozen dataclass would reject a new field. Use `decision.approval["approval_id"]`
rather than attribute access.

`stages` is the exception — a list of `StageResult` dataclasses, since that shape
is fixed.
:::

## Exceptions

| Exception | Meaning | Retry |
| --- | --- | --- |
| `VeeforeConfigurationError` | Required configuration missing | no |
| `VeeforeAuthenticationError` | Identity refused | no |
| `VeeforeBlockedError` | Policy refused. Carries `.decision` | **never** |
| `VeeforeApprovalRequiredError` | Human needed. Carries `.approval_id` | wait, do not retry |
| `VeeforeApprovalDeniedError` | A human said no. Carries `.approval_id` | no |
| `VeeforeRateLimitError` | `429` | yes, with backoff |
| `VeeforeConnectionError` | Transport failure | yes, with backoff |
| `VeeforeError` | Base class | depends |

```python
from veefore import VeeforeApprovalRequiredError, VeeforeBlockedError

try:
    decision = agent.act(request)
except VeeforeApprovalRequiredError as error:
    queue_for_human(error.approval_id)
except VeeforeBlockedError as error:
    report_refusal(str(error), error.decision)
```

## Assertion helpers

```python
from veefore import (
    ASSERTION_TTL_SECONDS,
    canonical_json,
    create_assertion,
    generate_nonce,
)
```

`canonical_json` matches the server's canonicalisation exactly — sorted keys, no
insignificant whitespace — which is what makes cross-language signing work.

## Synchronous, deliberately

The client is synchronous. A protected action is a single round trip whose
latency is dominated by the server, and the SDK is most often used inside a
framework that already manages its own concurrency.

For async code, run it in a thread:

```python
import asyncio

decision = await asyncio.to_thread(agent.act, request)
```

That keeps one well-tested code path rather than two that can drift.

## Differences from the TypeScript SDK

| | TypeScript | Python |
| --- | --- | --- |
| Request fields | `camelCase` | `snake_case` |
| Timeouts | `requestTimeoutMs`, `timeoutMs` | `request_timeout_seconds`, `timeout_seconds` |
| Nested decision fields | typed interfaces | plain `dict` |
| Concurrency | async | synchronous |

The wire format, the canonicalisation, the signatures and the server behaviour
are identical. There is no capability one SDK has and the other does not.

## Related

:::cards

:::card Protect an agent | /build/protect-an-agent
The full integration, both languages side by side.
:::

:::card Errors and retries | /build/errors-and-retries
Every error code mapped to an action.
:::

:::card TypeScript SDK | /build/javascript-sdk
The same surface, in TypeScript.
:::

:::card Sign your own assertion | /build/sign-your-own-assertion
The protocol with no SDK at all.
:::

:::
