Skip to main content

provio-sdk

Python integrations for provio. Every tool call an agent makes is checked against provio.yaml (allow / deny / ask / redact) before the tool runs, and recorded to provio's hash-chained ledger.

The package has no runtime dependencies. It starts provio check --stdio as a long-lived child process and speaks the hook-gateway protocol (docs/INTERFACES.md, Contract 6). Framework integrations import their framework only when you import them.

Install

The provio binary comes with it: provio-sdk depends on provio, whose platform wheels carry the prebuilt binary. No Rust toolchain or repository checkout is needed.

pip install provio-sdk                     # core: ProvioClient, Provio, @provio_tool
pip install "provio-sdk[langgraph]"        # + LangGraph / LangChain
pip install "provio-sdk[openai-agents]"    # + OpenAI Agents SDK
pip install "provio-sdk[claude-agent-sdk]" # + Claude Agent SDK

You also need the provio binary. The client looks for it in this order: the binary= argument, the PROVIO_BIN environment variable, then provio on PATH. policy= / ledger= become --policy / --ledger; otherwise provio uses ./provio.yaml and ./.provio/ledger.jsonl relative to the child's cwd.

Fail closed

The tool does not run unless provio answers dispatch: true. Every failure raises a ProvioError subclass, and each framework integration turns that into a refusal the model can read:

Situation Result
deny verdict ProvioDenied (rule, reason, provio.yaml:LINE)
ask, approver rejects / times out / raises ProvioApprovalRejected
ask with --ask deny (the default) ProvioApprovalRejected
provio binary missing or cannot start ProvioUnavailable
provio check exits while a request is waiting ProvioGatewayCrashed
no answer within timeout (child is killed) ProvioTimeout
non-JSON line, wrong id, contradictory response ProvioProtocolError
{"error": ...} response ProvioGatewayError
complete fails after the tool ran output withheld from the model
redact verdict but no redacted output returned output withheld from the model

A request is never retried: a retried decide could write a second Decision record for one call. A crashed gateway is restarted on the next request, up to max_restarts (default 3) per restart_window (60 s); past that the client refuses to start provio again.

Core API

from provio_sdk import Provio, provio_tool

provio = Provio(policy="provio.yaml", session_id="run-42")   # one provio check process

@provio.tool("fs.read")                 # the tool name your policy matches on
def read_file(path: str) -> str:
    return open(path).read()

read_file("README.md")                # decide -> run -> complete; ProvioDenied if refused
safe = provio.guarded(lambda command: ..., name="bash")
provio.execute("postgres.query", {"query": sql}, lambda: db.run(sql))  # redact -> redacted text

The sequence is always: decide; for a deferred ask, the approver plus resolve; run; complete. For a redact verdict the value returned is the redacted text provio sends back, not the original object.

ProvioClient / AsyncProvioClient expose the raw protocol (decide, resolve, complete) with typed Decision / Completion results. Both are thread-safe and share one child process; close() (or with) closes its stdin and waits for it to exit, and an atexit hook closes anything left open.

Approvals

With no approver, Provio runs --ask deny: every ask fails closed. Give it an approver and it runs --ask defer; the approver sees the call and the rule's diff, and its answer goes to provio with resolve:

from provio_sdk import Approval, Provio

def approve(req):                      # sync or async
    print(req.decision.rule_id, req.diff, req.call.args)
    return Approval(input("run it? [y/N] ") == "y", approver="human:alice")

provio = Provio(approver=approve, approval_timeout=120)   # timeout -> rejected

Anything other than True / Approval(True, ...) rejects, as do exceptions and timeouts (default: the rule's timeout_ms, else 300 s).

LangGraph / LangChain

Integration point: ToolNode(wrap_tool_call=..., awrap_tool_call=...). ToolNode hands every tool call to the wrapper with an execute callable; the wrapper asks provio first. A blocked call becomes a ToolMessage(status="error") with provio's reason, so the model sees the refusal and the graph keeps going. The session id is the graph's thread_id.

from langchain_core.tools import tool
from langgraph.prebuilt import tools_condition
from provio_sdk import Provio
from provio_sdk.langgraph import provio_tool_node

@tool
def read_file(path: str) -> str:
    """Read a file."""
    return open(path).read()

provio = Provio()
graph.add_node("tools", provio_tool_node([read_file], provio))   # instead of ToolNode([...])
graph.add_conditional_edges("model", tools_condition)
app = graph.compile(); app.invoke(inputs, {"configurable": {"thread_id": "t-1"}})

provio_tool_node accepts every ToolNode argument; your own wrap_tool_call runs outside provio's, so provio decides on the request as it will execute. It can also be passed as tools= to create_react_agent. For tools invoked outside a ToolNode, guard_tool(tool, provio) wraps a single BaseTool (use one or the other, not both: each records its own decision). For a redact verdict the ToolMessage content is replaced and its artifact dropped; a Command result under a redact rule is withheld.

OpenAI Agents SDK

Integration point: each FunctionTool's on_invoke_tool, wrapped. That is the call Runner awaits to execute a tool, so the wrapper can decide before the tool body runs, record complete, and substitute the redacted output. RunHooks.on_tool_start cannot refuse a single call (its return value is ignored; raising aborts the run), and tool input guardrails see no output, so they cannot carry a redact verdict.

from agents import Agent, RunConfig, Runner, function_tool
from provio_sdk import Provio
from provio_sdk.openai_agents import guard_agent

@function_tool
def read_file(path: str) -> str:
    """Read a file."""
    return open(path).read()

provio = Provio()
agent = guard_agent(Agent(name="coder", tools=[read_file]), provio)
result = await Runner.run(agent, "read README.md", run_config=RunConfig(group_id="t-1"))

A blocked call returns provio's refusal as the tool result. Session id: session_id= (string or ctx -> str), else RunConfig.group_id, else a session_id on your run context, else the Provio's. guard_agent refuses agents with tools it cannot gate (hosted tools, mcp_servers) unless you pass strict=False; put MCP servers behind provio proxy --mcp instead. Handoff targets are separate agents: guard each one. guard_function_tool wraps a single tool.

Claude Agent SDK

Integration point: SDK hooks. PreToolUse decides (allow / deny with provio's reason), PostToolUse records complete and, for a redact verdict, returns updatedToolOutput with the redacted result, PostToolUseFailure records the failure. can_use_tool is not used: the CLI only consults it when its own permission rules would prompt, and it has no post-execution step.

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from provio_sdk import Provio
from provio_sdk.claude_agent_sdk import provio_hooks

provio = Provio()
options = ClaudeAgentOptions(hooks=provio_hooks(provio))
async with ClaudeSDKClient(options) as client:
    await client.query("list the repo and summarize README.md")
    async for message in client.receive_response():
        print(message)

Tool names are mapped exactly as provio check --format claude-code maps them (toolmap.py), so one provio.yaml covers Claude Code and the SDK:

Claude tool provio tool policy field
Bash, PowerShell bash command
Read, Glob, Grep, LS, NotebookRead fs.read path (from file_path)
Write, Edit, MultiEdit, NotebookEdit fs.write path (from file_path / notebook_path)
WebFetch http url (url.host)
WebSearch web.search query
mcp__<server>__<tool> <tool> server = <server>
anything else unchanged unchanged

Redacted output keeps the tool's output shape (Claude Code rejects a mismatched updatedToolOutput for built-in tools); if the redacted result cannot be parsed back into the same shape, every string in the output is masked. The hooks never raise; any adapter failure answers deny.

Development

python -m venv .venv && .venv/Scripts/python -m pip install -e ".[dev]"   # bin/ on POSIX
.venv/Scripts/python -m pytest
PROVIO_E2E=1 PROVIO_BIN=/path/to/provio .venv/Scripts/python -m pytest tests/test_e2e_provio.py

Unit tests run against tests/fake_provio.py, a small Contract 6 gateway that picks verdicts by tool name and can misbehave on request (malformed lines, hangs, crashes, contradictions). Framework tests use each framework's real runtime with a scripted model: a LangGraph StateGraph, the Agents SDK's agents.testing.ScriptedModel, and a scripted stand-in for the Claude Code CLI behind the SDK's Transport. No network, no API keys. The e2e module is skipped unless PROVIO_E2E=1; it builds nothing and uses PROVIO_BIN.

Tested with Python 3.14, langgraph 1.2.12 / langchain-core 1.6.4, openai-agents 0.22.3, claude-agent-sdk 0.2.157. Requires Python >= 3.10.

License

Apache-2.0. See LICENSE.

Metadata

Release files for provio-sdk 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for provio-sdk 0.1.3
File Size Uploaded
provio_sdk-0.1.3.tar.gz 41.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for provio-sdk 0.1.3
File Interpreter ABI Platform
provio_sdk-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 78.4 kB

Release files / provio_sdk-0.1.3.tar.gz

Download URL provio_sdk-0.1.3.tar.gz
Size 41.9 kB
Tags Source
SHA-256 checksum
How to use checksums
a261d4c0d034e9c3c16187918877f6b8bdaf7d6bf2e81f6f23278c22f0b8d694
BLAKE2b-256 checksum
How to use checksums
10d60a6bff53a062f3d1fa2410cce6465317cd04d350210b8560fa0fa57c6f1e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / provio_sdk-0.1.3-py3-none-any.whl

Download URL provio_sdk-0.1.3-py3-none-any.whl
Size 36.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a2faf5ac67700aacdcde8b333fa0e1e385ed0775ab22c1fd5c806f07c73a95a0
BLAKE2b-256 checksum
How to use checksums
88fedffbe99d47236115e41340a6f18dd2520da510b75839889388bf6c0259af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.1.5

2 release files

0.1.4

2 release files

This release

0.1.3 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page