Skip to main content

Policy-gated execution for AI agents

Project description

Provenance Python SDK

Policy-gated execution for AI agents. Every tool call is evaluated against your tenant's active policy before it runs, producing an immutable audit trail of ALLOW, BLOCK, and human-escalated decisions.

Installation

pip install provenance-client
# or with uv
uv add provenance-client

Requires Python 3.13+ · Licensed under GNU GPL v2


Authentication

The SDK authenticates via an API key scoped to a tenant policy. Generate one from the Provenance dashboard (or the API docs) and pass it at client construction.

from provenance_client import ProvenanceClient, ProvenanceGateway

gateway = ProvenanceGateway(
    ProvenanceClient(
        gateway_url="http://localhost:4587",
        agent_id="<your-agent-id>",
        api_key="pk_live_...",
    )
)

The key is sent as X-PROVENANCE-API-KEY on every request. It encodes your tenant and active policy.


Usage patterns

1. Direct execute

The most explicit pattern. Call execute() before performing any agent action; inspect the result before proceeding.

from provenance_client import Decision, ProvenanceClient, ProvenanceGateway
from provenance_client.services import PolicyBlockedError

gateway = ProvenanceGateway(
    ProvenanceClient(
        gateway_url="http://localhost:4587",
        agent_id="<agent-id>",
        api_key="pk_live_...",
    )
)

try:
    result = gateway.execute(
        "payments.initiate",
        {"amount": 50, "currency": "GBP", "recipient_id": "rec_abc123"},
        decision=Decision.ALLOW,
    )
    print(result.decision)   # Decision.ALLOW
    print(result.reason)     # "Payment within approved parameters"
    print(result.event_id)   # immutable audit log entry ID
except PolicyBlockedError as e:
    print(e.reason)          # human-readable block reason
    print(e.event_id)        # audit event ID for the blocked call

2. @guard decorator

Gates any callable behind policy evaluation. Provenance intercepts the call before the function body runs. Works transparently on both sync and async functions.

@gateway.guard("payments.initiate")
def initiate_payment(amount: float, currency: str, recipient_id: str) -> dict:
    # Only reached if policy allows it
    return payment_service.create(amount, currency, recipient_id)

@gateway.guard("email.send")
async def send_email(to: str, template: str) -> dict:
    # Only reached if policy allows it
    return await mailer.send(to, template)

Call them like normal functions:

try:
    result = initiate_payment(50, "GBP", "rec_abc123")
except PolicyBlockedError as e:
    print(f"Blocked: {e.reason}")

The decorator captures all call-site arguments and includes them in the audit log automatically.

3. Session context manager

Groups a series of tool calls under a shared session_id. All events in the session are correlated in the audit log. On exit, the session logs a summary.

with gateway.session("sess_checkout_flow") as sess:
    sess.execute(
        "payments.initiate",
        {"amount": 50, "currency": "GBP", "recipient_id": "rec_abc123"},
        decision=Decision.ALLOW,
    )
    sess.execute(
        "email.send",
        {"to": "customer@example.com", "template": "receipt"},
        decision=Decision.ALLOW,
    )

    print(sess.allowed_count)   # 2
    print(sess.blocked_count)   # 0
    print(sess.results)         # list[ExecutionResult]

4. Async variants

All three patterns have async counterparts. async_execute is used directly; @guard auto-detects async functions.

result = await gateway.async_execute(
    "payments.initiate",
    {"amount": 50, "currency": "GBP", "recipient_id": "rec_abc123"},
    decision=Decision.ALLOW,
)

Escalation

When a policy rule evaluates to ESCALATE, execution is held and a human reviewer is notified. The async_execute call blocks (up to timeout seconds) waiting for a human decision. The resolved decision — ALLOW or BLOCK — is returned as the final ExecutionResult.

import asyncio

holder = {}

async def initiate_large_payment():
    holder["result"] = await gateway.async_execute(
        "payments.initiate",
        {"amount": 800, "currency": "GBP", "recipient_id": "rec_xyz789"},
        decision=Decision.ESCALATE,
    )

# The execution is held server-side; your reviewer approves via the dashboard
# or the escalation API while this coroutine is suspended.
await asyncio.gather(
    initiate_large_payment(),
    reviewer.approve(escalation_id, "Verified with CFO"),
)

r = holder["result"]
print(r.decision)        # Decision.ALLOW (after approval)
print(r.actor_human_id)  # identity of the human who approved
print(r.escalation_id)   # escalation record ID

If the escalation is rejected, async_execute raises PolicyBlockedError. Always catch it inside the coroutine when using asyncio.gather:

async def initiate_jpy_payment():
    try:
        holder["result"] = await gateway.async_execute(
            "payments.initiate",
            {"amount": 100, "currency": "JPY", "recipient_id": "rec_jpy001"},
            decision=Decision.ESCALATE,
        )
    except PolicyBlockedError as e:
        holder["result"] = e   # store for inspection after gather

await asyncio.gather(
    initiate_jpy_payment(),
    reviewer.reject(escalation_id, "JPY not authorised"),
)

if isinstance(holder["result"], PolicyBlockedError):
    print(f"Rejected: {holder['result'].reason}")

ExecutionResult

Returned by every execution path.

Field Type Description
decision Decision ALLOW, BLOCK, or ESCALATE
reason str Human-readable explanation from the policy engine
action str The action string that was evaluated
event_id str Immutable audit log entry ID
escalation_id str | None Set if the call was escalated
actor_human_id str | None Identity of the human approver (if applicable)
tool_result Any Return value of the wrapped function (set by @guard)

Convenience properties: .allowed, .blocked, .escalatedbool


Exception reference

Exception Raised when
PolicyBlockedError Policy evaluates to BLOCK, or an escalation is rejected
EscalationTimeoutError Escalation hold expires with no human decision (60 s default)
EscalationError Escalation is still in progress when queried
GatewayError Gateway unreachable and on_gateway_error="closed" (default)

All inherit from ProvenanceError.

from provenance_client.services import (
    PolicyBlockedError,
    EscalationTimeoutError,
    GatewayError,
)

try:
    result = gateway.execute("payments.initiate", {...})
except PolicyBlockedError as e:
    # e.action, e.reason, e.event_id
except EscalationTimeoutError as e:
    # e.action, e.escalation_id
except GatewayError as e:
    # e.url, e.cause

ProvenanceClient reference

ProvenanceClient(
    gateway_url="http://localhost:4587",  # base URL of the Provenance gateway
    agent_id="<agent-id>",               # agent identity for audit logs
    api_key="pk_live_...",               # API key scoped to a tenant policy
    on_gateway_error="closed",           # "closed" (raise) | "open" (allow through)
    default_session=None,                # shared session_id; auto-generated if omitted
    timeout=90.0,                        # seconds — set high for escalations
)

Fail-open mode

By default (on_gateway_error="closed"), a gateway connection failure raises GatewayError. Set on_gateway_error="open" to allow all calls through when the gateway is unreachable, with a warning:

client = ProvenanceClient(
    gateway_url="http://localhost:4587",
    agent_id="<agent-id>",
    api_key="pk_live_...",
    on_gateway_error="open",
)

Resource cleanup

Close the underlying HTTP client when your application shuts down:

# Sync
client.close()

# Async
await client.aclose()

Environment variables

Store credentials in the environment rather than hardcoding them:

export PROVENANCE_GATEWAY_URL="http://localhost:4587"
export PROVENANCE_AGENT_ID="<agent-id>"
export PROVENANCE_API_KEY="pk_live_..."
import os
from provenance_client import ProvenanceClient, ProvenanceGateway

gateway = ProvenanceGateway(
    ProvenanceClient(
        gateway_url=os.environ["PROVENANCE_GATEWAY_URL"],
        agent_id=os.environ["PROVENANCE_AGENT_ID"],
        api_key=os.environ["PROVENANCE_API_KEY"],
    )
)

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

provenance_client-0.1.0.tar.gz (16.3 kB view details)

Uploaded Source

Built Distribution

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

provenance_client-0.1.0-py3-none-any.whl (18.4 kB view details)

Uploaded Python 3

File details

Details for the file provenance_client-0.1.0.tar.gz.

File metadata

  • Download URL: provenance_client-0.1.0.tar.gz
  • Upload date:
  • Size: 16.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for provenance_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 211710195825a8a162f57cc95c96fce687663075bd14dfb77cb1b449a7a7b084
MD5 06813fedc48766b5b71405daaf017f63
BLAKE2b-256 bff0f6e1e1c9f1e6e2d4285cf27b1aa4ec76a826d2951d267c43d8aa18540937

See more details on using hashes here.

File details

Details for the file provenance_client-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: provenance_client-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for provenance_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 51559ade170e07ea68fe29a50ed3929fb1c99f985043f8b503b9293fcdeeebfb
MD5 9764319a3706c6f7ace0ffc5a2916d53
BLAKE2b-256 ed16ea9c19b0ecfd890ecb67d96b6c0a355ea4d33f826f3d4e274856b2dea3f9

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