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.

NullRun dashboard home showing the workflow control panel. NullRun dashboard home showing the workflow control panel.

How it fits together

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

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 is a BaseException so it 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
nullrun.init() patches OpenAI, Anthropic, LangGraph, OpenAI Agents, Mistral, Gemini, Cohere, Bedrock, LlamaIndex, CrewAI, and AutoGen — cost tracking without @protect.
Decision history + audit chain
Every gate decision (allow / block / require_approval) is recorded in audit_events with hash-chained content_hash + previous_hash. The chain is recompute-verifiable on demand via GET /api/v1/orgs/{org_id}/audit-log/verify.

Wire it up in 30 lines

sequenceDiagram
  participant U as Your code
  participant SDK as nullrun SDK
  participant G as NullRun gateway
  participant DB as Dashboard

  U->>SDK: from nullrun import init, protect
  U->>SDK: init(api_key="nr_live_...")
  Note over SDK: fetches HMAC secret via /api/v1/auth/verify

  U->>SDK: with workflow("user-123"):<br/>  @protect<br/>  def step(): ...

  loop every @protect call
    U->>SDK: step()
    SDK->>G: POST /api/v1/gate (tokens=1)
    G-->>SDK: {decision: "allow"}
    SDK->>SDK: run wrapped function
    SDK->>G: POST /api/v1/track (actual cost)
  end

  DB->>G: operator clicks Kill
  G-->>SDK: WS push: StateChange(killed)
  SDK->>U: raise WorkflowKilledInterrupt

Managed runtime, not a self-hosted deployment

NullRun runs as a managed control plane at nullrun.io. There is no self-hosted deployment option today. The Python SDK runs inside your process and talks to the hosted gateway over HTTPS; the dashboard at nullrun.io hosts the control plane. See the docs for the SDK surface and /about for the runtime contract.