Metadata-Version: 2.4
Name: pvd
Version: 0.4.3
Summary: Send your agents' AI traffic through the Paved gateway
Author: Paved (https://hipaved.com)
License: Proprietary
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: click>=8.1
Requires-Dist: requests>=2.31
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: black; extra == "dev"
Provides-Extra: openai
Requires-Dist: openai>=1.40; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.40; extra == "anthropic"
Provides-Extra: litellm
Requires-Dist: litellm>=1.40; extra == "litellm"
Provides-Extra: langchain
Requires-Dist: langchain-openai>=0.2; extra == "langchain"
Provides-Extra: all
Requires-Dist: openai>=1.40; extra == "all"
Requires-Dist: anthropic>=0.40; extra == "all"
Requires-Dist: litellm>=1.40; extra == "all"
Requires-Dist: langchain-openai>=0.2; extra == "all"

# Paved

**The Python SDK for the Paved platform — the governed gateway your agents call through.**

Paved is an operating layer for an organization's real work: agents run the routine middleware of a function and escalate the genuine judgment calls to humans, whose resolutions are captured as reusable knowledge. This SDK is the **on-ramp** to that platform — it routes your LLM calls, tool executions, and API requests through Paved's policy engine, so you get security, credential injection, and audit trails without changing your agent code. Governance is the entry point; the platform's escalation → resolution loop and autonomy ladder build on top of it (see the platform docs).

Paved sits between your AI agents and the APIs they call. Every request flows through the policy engine, checked `allow` / `deny` / `flag` before it reaches a provider.

**Dashboard**: [app.hipaved.com](https://app.hipaved.com) | **Docs**: [docs.hipaved.com](https://docs.hipaved.com) | **Website**: [hipaved.com](https://hipaved.com)

---

## How It Works

```
Your agent --> Paved gateway --> policy check (Guard API -> OPA) --> LLM provider or integration API
                    |
                    +--> provider keys and credentials injected, output checked, audit log
```

1. Connect LLM providers (model routing) and integrations in the dashboard.
2. Define policies in the dashboard. Each check returns `allow`, `deny` or `flag`.
3. Send traffic through the gateway with this SDK. Your code holds one Paved key and no provider keys.
4. Read audit logs, cost and policy results in the dashboard.

A denied call is blocked and raises `PolicyDeniedError`. A flagged call proceeds and is logged for review.

**Gateway-first.** Every surface in this SDK sends traffic to the gateway. No surface calls a provider
directly, and no surface accepts a provider key. The policy check happens on the server, where the
client cannot skip it.

| Surface | Use it for | Endpoint |
|---------|-----------|----------|
| `pvd.openai.OpenAI` / `AsyncOpenAI` | OpenAI-shaped model calls | `/v1/gateway/chat/completions`, `/embeddings`, ... |
| `pvd.anthropic.Anthropic` / `AsyncAnthropic` | Anthropic-shaped model calls | `/v1/gateway/v1/messages` |
| `pvd.litellm.LiteLLM` | LiteLLM code | `/v1/gateway/chat/completions`, `/embeddings` |
| `pvd.langchain.ChatPaved` | LangChain and LangGraph | `/v1/gateway/chat/completions` |
| `Agent.proxy()` | Integration API calls with stored credentials | `/v1/gateway/proxy` |
| `Agent.agent()` | A tool loop that the gateway runs | `/v1/gateway/agent` |
| `Agent.check()`, `@tool`, `@guard_tool` | Your own tools and actions | `/v1/policies/check` |

---

## Install

```bash
pip install pvd                # core: Agent, tool, guard_tool, CLI
uv add pvd                     # the same, in a uv project
pip install "pvd[openai]"      # also: anthropic, litellm, langchain, all
```

Python 3.10 or newer. The core package needs `click` and `requests`.

API keys start with `pvd_live_`. Create one in the dashboard.

---

## Configuration

| Variable | Meaning | Default |
|----------|---------|---------|
| `PAVED_API_KEY` | Your `pvd_live_...` key | none (required) |
| `PAVED_URL` | Platform API base | `https://app.hipaved.com/v1` |
| `PAVED_PROJECT` | Project slug or ID, sent as `X-Paved-Project` | none |

Every surface reads the same three variables. Arguments override them:
`paved_api_key=`, `paved_url=`, `project=` (on `Agent`: `api_key=`, `base_url=`, `project=`).

`PAVED_URL` may end in `/v1` or not; the SDK adds it. `PAVED_API_URL` still works as an older name.
Use the app host (`app.hipaved.com`), not `api.hipaved.com`. The `api.` host serves the Guard API only.

For local development, set `PAVED_URL=http://localhost:8090`.

### Stream policy

The model clients take `stream_policy=` (sent as `X-Paved-Stream-Policy`):

- `progressive` (default on the server): chunks flow; the gateway checks the output as it grows.
- `buffered`: the gateway checks the full output before it sends the first chunk.
- `passthrough`: no output check on streams. The key needs the `stream:passthrough` scope.

---

## Model calls

### OpenAI

```python
from pvd.openai import OpenAI, PolicyDeniedError   # also AsyncOpenAI

client = OpenAI(project="payments")   # PAVED_API_KEY from the environment
try:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Summarize our Q3 results"}],
    )
except PolicyDeniedError as e:
    print(e.phase, e.reasons, e.request_id)
```

This is the real `openai.OpenAI` with its base URL set to the gateway. Every method works as before.
The gateway routes by model name, so `model="claude-sonnet-4-5"` also works if model routing has it.

Passing `api_key=` raises `TypeError`. Provider keys live in model routing on the platform.

### Anthropic

```python
from pvd.anthropic import Anthropic   # also AsyncAnthropic

client = Anthropic(stream_policy="buffered")
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Analyze this contract"}],
)
```

The Paved key is sent as `x-api-key`. Passing `api_key=` or `auth_token=` raises `TypeError`.

### LiteLLM

```python
from pvd.litellm import LiteLLM

llm = LiteLLM(project="payments")
llm.completion(model="gpt-4o", messages=[{"role": "user", "content": "Hello"}])
llm.embedding(model="text-embedding-3-small", input="Hello world")
```

Methods: `completion`, `acompletion`, `embedding`, `aembedding`. Each call goes to the gateway as
`openai/<model>`. Passing `api_key`, `api_base` or `base_url` raises `TypeError`.

### LangChain and LangGraph

```python
from pvd.langchain import ChatPaved

llm = ChatPaved(model="gpt-4o-mini", project="payments")
llm.invoke("Hello")
```

`ChatPaved` returns a `langchain_openai.ChatOpenAI` that points at the gateway. Tools, streaming
and LangGraph work as they do with `ChatOpenAI`.

### Denials

A policy denial from the gateway raises `PolicyDeniedError`. The OpenAI and Anthropic clients raise a
class that is both `pvd.PolicyDeniedError` and the provider's `PermissionDeniedError`, so existing
`except` blocks still work. The error carries:

- `e.phase`: `input` or `output`
- `e.reasons`: a list of strings
- `e.request_id`: the audit log ID
- `e.decision`: the full response body

A 403 that is not a policy denial (for example, a missing project) stays the provider's normal error.

Known gap: a denial in the middle of a stream arrives as an error chunk. It is not yet mapped to
`PolicyDeniedError`.

### curl

```bash
curl -X POST https://app.hipaved.com/v1/gateway/chat/completions \
  -H "Authorization: Bearer pvd_live_..." \
  -H "X-Paved-Project: payments" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}]}'
```

---

## Integration calls: `Agent.proxy()`

```python
from pvd import Agent

agent = Agent(agent_id="my-agent", project="payments")

repos = agent.proxy("github", "GET", "/user/repos")
agent.proxy("github", "POST", "/repos/acme/app/issues", body={"title": "Bug: login broken"})
accounts = agent.proxy(
    "salesforce", "GET", "/services/data/v59.0/query",
    query={"q": "SELECT Id, Name FROM Account LIMIT 10"},
)
agent.proxy("stripe", "GET", "/v1/charges", on_behalf_of={"role": "support", "department": "cs"})
```

Signature: `proxy(resource, method, path, body=None, query=None, headers=None, on_behalf_of=None, timeout=30.0)`.
`resource` is an integration slug or a resource UUID. Paved checks resource-access policy, adds the
stored credentials and forwards the request. Credentials stay on the platform.

The call returns the platform response:

```python
{
    "status_code": 200,
    "body": [...],        # upstream response
    "headers": {...},
    "decision": "allow",
    "reasons": [],
    "request_id": "...",
    "latency_ms": 142,
}
```

- `PolicyDeniedError`: the decision is `deny`.
- `ProxyError`: a transport error, a non-JSON reply, or a reply with no decision (for example, HTTP 401).

---

## Gateway agent runs: `Agent.agent()`

The gateway turns each integration's actions into LLM tools and runs the tool loop.
It checks policy on every tool call and returns the final answer with a trace.

```python
result = agent.agent(
    messages=[{"role": "user", "content": "Get me my top 5 Salesforce accounts"}],
    integrations=["salesforce"],
    model="gpt-4o",
)

print(result.content)
print(result.finish_reason)    # "stop", "max_rounds" or "policy_denied"
print(result.tool_executions)  # one trace per tool call
print(result.denied_tools)     # the traces that policy denied
```

Parameters: `messages`, `integrations`, `model="gpt-4o"`, `max_rounds=10` (1 to 50),
`temperature`, `max_tokens`, `on_behalf_of`, `system`, `timeout=120.0`.

A denied input raises `PolicyDeniedError`. A denied tool call does not raise; the run continues
and the trace shows the denial. Other failures raise `AgentError`.

`on_behalf_of` passes end-user identity to OPA, so policies can apply per-user rules.
The standard fields are `user_id`, `role`, `department` and `clearance`. Other fields reach OPA unchanged.

### AgentResult

```python
result.content          result.finish_reason    result.rounds_used
result.tool_executions  result.usage            result.latency_ms
result.request_id       result.model
result.prompt_tokens    result.completion_tokens    result.total_tokens
result.successful_tools   # no deny, no error
result.denied_tools       # denied by policy
result.to_dict()          # raw response
```

---

## Your own tools: `check`, `@tool`, `@guard_tool`

Use these for actions that do not go through the gateway, such as a function in your own code.

```python
from pvd import Agent, PolicyDeniedError, guard_tool, tool

agent = Agent(agent_id="my-agent", policies=["default"])

try:
    agent.check("email", {"to": ["ceo@company.com"], "subject": "Q3 Report"})
    send_email(...)
except PolicyDeniedError as e:
    print(f"Blocked: {e.reasons}")


@tool(policies=["pii_strict"])          # the first argument is the Agent
def lookup_customer(agent, customer_id): ...

@guard_tool(name="refund", agent=agent)  # or bind the Agent once
def refund(order_id, amount): ...
```

- `check(action, payload, policies=None)` returns the decision on `allow` or `flag` and raises on `deny`.
- `policies=` on a check or a decorator replaces the agent's default policies for that call.
- The tool payload is `{"tool_name", "args", "kwargs"}`. The Agent itself is never in it. Values that
  are not JSON become strings.
- A denied tool does not run.
- If the platform cannot be reached, the check fails closed: the decision is `deny`.
- Any decision other than `allow` or `flag` counts as `deny`.

### Governance summary

Every check, proxy call and agent tool execution is recorded once on the `Agent`.

```python
summary = agent.get_governance_summary()
summary["total_checks"], summary["allowed_actions"], summary["denied_actions"], summary["flagged_actions"]
summary["violations"]         # the denied records
summary["overall_decision"]   # "deny" if any deny, else "flag" if any flag, else "allow"
```

`agent.get_policy_decisions()` returns the raw list. The record lives in memory only.
The SDK sends nothing in the background and nothing at exit; the audit trail lives on the platform.

---

## CLI

```bash
pvd check                  # does PAVED_API_KEY reach the gateway?
pvd login                  # email + password, for `pvd list`
pvd list --limit 20        # agents in your projects
pvd logout
pvd --version
```

`--platform <url>` overrides the platform URL for one command. The CLI reads `PAVED_URL`
(then `PAVED_PLATFORM_URL`) and stores its login in `~/.paved/config.json`.

---

## Session capture and governance hooks: `pvd capture`

For Claude Code and Codex seats on a subscription login, which do not go through the gateway.
`pvd capture` installs two kinds of hook:

- **Capture** (`Stop`, `SubagentStop`, `PreCompact`, `SessionEnd`): sends each session's
  transcript after each turn.
- **Governance** (`PreToolUse`, `UserPromptSubmit`): checks each tool call and each prompt
  against the project's policies before it runs. A `deny` or `redact` verdict stops it with
  the reason. `allow` and `flag` answer nothing, so the person's own permission prompt stays.

Make your own ingest key (`pvd_ing_...`) in Paved, on the **Claude Code & Codex** page, in a
project you belong to. (When your org turned that off, an org admin makes it.) Then, in your
own terminal, install with one of these. Each one gives the same `pvd capture` command.

```bash
# npm (Node 18+): no Python needed
npx -y @paved-ai/pvd@latest capture install  # asks for the key; never pass it as an argument

# uv
uv tool install pvd
pvd capture install

# pip
pip install pvd
pvd capture install

pvd capture status                 # with npm: npx -y @paved-ai/pvd@latest capture status
```

`pvd capture ...` and `pvd-capture ...` are the same command. Do not use `uvx`: it runs from
a temporary environment, and the hooks need a fixed one. The npm package `@paved-ai/pvd`
(repo `paved-ts-sdk`) holds a Node port of `src/pvd/capture.py`. It copies itself to
`~/.paved/capture/pvd-capture.mjs` and the hooks run that copy. Both write the same files,
and either one's `uninstall` removes the other's hooks.

`install` checks the key with Paved, saves it to `~/.paved/capture/key` (mode 0600), and adds
the hooks to `~/.claude/settings.json` and `~/.codex/hooks.json` (`CLAUDE_CONFIG_DIR` and
`CODEX_HOME` move them). It keeps your own hooks and settings, makes a one-time
`.pvd-backup`, and running it again gives the same file. Restart open sessions after. Codex
runs a hook only after you trust it: open Codex, run `/hooks`, and trust the Paved hooks.

Codex runs hooks in its background app-server, which does not see your shell's environment.
So `install` saves the key and the URL to files even when they came from `PAVED_INGEST_KEY`
or `PAVED_URL`, and a non-default `PAVED_CAPTURE_HOME` goes on the hook command as `--home`.

```bash
pvd capture install --dry-run          # print the files it would write; write nothing
pvd capture install --client codex     # one client (claude-code, codex, all)
pvd capture install --no-governance    # capture only (or --no-capture)
pvd capture install --key-stdin < key  # for scripts
pvd capture uninstall [--keep-key]     # take out only the Paved hooks
pvd capture flush                      # send the spool now
```

A governance hook fails open: when Paved is down or slow (3.5 s), the action goes ahead.
When the org chose fail closed, `install` writes `check --fail closed` and a failed check
blocks. The capture hook returns at once and sends from a detached process. It never writes
to stdout and always exits 0. When Paved is down, chunks wait in `~/.paved/capture/spool`
(200 MB cap). Errors go to `~/.paved/capture/log`. `src/pvd/capture.py` uses the standard
library only and also runs as one file: `python3 capture.py install`.

---

## Changes in 0.4.3

- `pvd capture install` says who reads what a seat sends: only you read the words of
  your own calls; others in your org see metadata, unless the org shares the words.

## Changes in 0.4.2

- README only: the npm package is `@paved-ai/pvd` (npm refused `pvd`).

## Changes in 0.4.1

- `pvd capture ...` runs `pvd-capture ...`, so the command is the same as in the npm package.
- The npm package `@paved-ai/pvd` (Node 18+): `npx -y @paved-ai/pvd@latest capture install`. The hooks it writes
  and the files it keeps are the same as the Python command's.
- The README names `uv tool install pvd`.

## Changes in 0.4.0

- `pvd-capture install` and `uninstall` add and remove the Paved hooks in Claude Code and
  Codex settings. You no longer edit the JSON by hand.
- `pvd-capture check` is the governance hook: the project's policies check each prompt and
  tool call before it runs.

## Changes in 0.3.1

- `agent.proxy()` raises `ProxyError` when the call fails before it reaches the upstream, for example a resource that is set up wrong. A policy denial still raises `PolicyDeniedError`.

---

## Upgrading from 0.2

0.3.0 is a breaking release. Every surface now sends traffic through the gateway.

- The OpenAI, Anthropic and LiteLLM wrappers no longer accept a provider key. The SDK-check mode is gone.
- `agent_id=` is gone from the model clients. Use `project=` to pick the project.
- `PavedCallbackHandler` is gone. Use `ChatPaved`.
- `agent.llm()`, `agent.http_request()` and `agent.send_email()` are gone. They returned mock values.
- Telemetry and `agent.complete()` are gone. The gateway audit log is the record.
- LiteLLM `image_generation` and the `*_with_streaming` methods are gone. Pass `stream=True`.
- The CLI keeps `login`, `logout`, `list` and adds `check`. The deploy and runtime commands are gone.

---

## Links

- Dashboard: [app.hipaved.com](https://app.hipaved.com)
- Docs: [docs.hipaved.com](https://docs.hipaved.com)
- Website: [hipaved.com](https://hipaved.com)
- PyPI: [pypi.org/project/pvd](https://pypi.org/project/pvd/)
