Skip to content

Runtime decision layer for tool-using AI agents

Before your agent executes a supported tool or model call, the SDK asks the gate. allow, block, or require_approval — backed by tool patterns, budgets, rate limits, and human approvals.

pip install nullrun
your_tool.py
import nullrun

nullrun.init()


@nullrun.protect
def refund_customer(order_id: str, amount_cents: int) -> str:
    """The gate answers before this body runs."""
    return payments.refund(order_id, amount_cents)

Decorate the tool. The gate evaluates the call; your function runs only if the decision is allow — and waits for a human if the policy says require_approval.

NullRun dashboard home showing the workflow control panel. NullRun dashboard home showing the workflow control panel.
Every gate decision, budget reservation, and cost event lands in the dashboard. That is the whole surface an operator touches.

How it fits together

flowchart LR
  Agent["Your agent<br/>(Python SDK)"] -->|"@protect"| Gateway["NullRun gateway"]
  Gateway -->|"budget pre-flight<br/>policy fetch"| Decision{"allow?"}
  Decision -->|"yes"| Body["wrapped function runs"]
  Decision -->|"no"| Block["raise NullRunBlockedException"]
  Operator["operator"] -.->|"kill / pause"| Gateway
  Gateway -.->|"control plane<br/>(WebSocket)"| Agent

What you get out of the box

Budget gate
Set a per-workflow cap in cents. The SDK asks the gateway "any budget left?" before every @protect call — no round-trip cost when the answer is "yes". Hard blocks on overrun; soft mode allows a bounded overrun when an active chain is present.
Action-bound approvals
Operator approves sensitive calls via typed predicates (money_amount / tool_parameters). Every approval is bound to the exact action payload via a SHA-256 action_digest — the grant is refused if the SDK then executes a different amount or different arguments.
Real-time kill / pause
A WebSocket control plane pushes killed / paused to every connected SDK. WorkflowKilledInterrupt reaches the top of the agent loop, not a swallowed except Exception.
ToolBlock policy
Server-side glob-pattern rules (mcp://payments/refund*, bash, db.drop) decide which canonical tool names are allowed. Always Hard: fails closed on transport error, regardless of the budget's enforcement_mode.
Auto-instrumentation
The first @protect call activates auto-instrumentation for OpenAI, Anthropic, LangGraph, OpenAI Agents, Mistral, Gemini, Cohere, Bedrock, LlamaIndex, CrewAI, and AutoGen. Vendor SDKs are patched in place — every LLM call goes through the gateway.
Audit chain
Every gate decision (allow / block / require_approval) is recorded in an append-only audit log with a tamper-evident hash chain. The chain is recompute-verifiable on demand via the audit-log verify endpoint.

What one protected call looks like

sequenceDiagram
  participant A as Your code
  participant S as NullRun SDK
  participant G as NullRun gateway
  participant O as Operator

  A->>S: run a protected call
  S->>G: gate check (tool name, projected cost)
  G-->>S: allow
  S->>A: run the wrapped function
  S->>G: track the actual cost
  O->>G: kill or pause the workflow
  G-->>S: state change over the control plane
  S-->>A: raise WorkflowKilledInterrupt

End-to-end: the gate evaluation, cost accounting, and the kill path.

The Python SDK runs inside your own process and talks to the NullRun gateway over HTTPS. Every gate decision, budget reservation, and cost event travels over that connection — the SDK adds a network round-trip and nothing else to your deployment. Policies, workflows, approvals, and the operator controls live in the dashboard, which is where your team configures what the gate enforces.