Skip to main content

Policy-as-code runtime boundaries for Python AI agents

Project description

Boundari

Policy-as-code runtime boundaries for Python AI agents.

Boundari gives AI agents hard runtime boundaries: tool permissions, approval gates, budgets, schemas, redaction, and audit logs without forcing you into a new agent framework.

It is designed to sit around tools from Pydantic AI, OpenAI Agents SDK, LangGraph, CrewAI, or a custom Python loop. Your agent can reason freely, but before it mutates the world, Boundari checks the contract.

Install

pip install boundari

For local development:

uv venv
uv sync --extra dev
uv run pytest

Quickstart

from boundari import Boundary, Budget, ToolPolicy

def send_email(to: str, subject: str, body: str) -> dict[str, str]:
    return {"message_id": "msg_123", "status": "sent", "to": to}

boundary = Boundary(
    name="support_agent",
    budget=Budget(max_tool_calls=20, max_runtime_seconds=180),
    tools=[
        ToolPolicy("email.send").require_approval(
            when="recipient_domain not in trusted_domains"
        ),
        ToolPolicy("docs.search").allow(),
        ToolPolicy("shell.run").deny(),
    ],
    trusted_domains=["example.com"],
)

safe_send_email = boundary.wrap_tool("email.send", send_email)

result = safe_send_email(
    to="customer@outside.test",
    subject="Refund update",
    body="We can help with that.",
)

assert result.allowed is False
assert result.reason == "approval_denied"

Boundari returns a structured Decision for denied calls. If you prefer exceptions, wrap with raise_on_denied=True.

Core Concepts

Boundary is the runtime contract for one agent or workflow.

ToolPolicy says whether a named tool is allowed, denied, schema-validated, approval-gated, table-scoped, or amount-limited.

RunContext tracks per-run budgets such as tool calls, runtime, tokens, and cost.

Redactor masks sensitive strings before tool outputs are returned to the model.

AuditEvent records allow, deny, approval, and output decisions in a structured way.

Python API

from decimal import Decimal

from pydantic import BaseModel, EmailStr

from boundari import Boundary, Budget, ToolPolicy


class EmailInput(BaseModel):
    to: EmailStr
    subject: str
    body: str


class EmailResult(BaseModel):
    message_id: str
    status: str
    to: EmailStr


boundary = Boundary(
    name="customer_support_agent",
    budget=Budget(max_tool_calls=12, max_runtime_seconds=120, max_cost_usd=Decimal("0.25")),
    tools=[
        ToolPolicy("zendesk.search").allow(),
        ToolPolicy("zendesk.reply").require_approval(),
        ToolPolicy("stripe.refund").require_approval().max_amount("100.00"),
        ToolPolicy("email.send").input(EmailInput).output(EmailResult),
        ToolPolicy("shell.run").deny(),
    ],
)

Decorator API

Use boundary_tool to attach Boundari metadata to a plain callable. Framework adapters can inspect this metadata, and you can also pull the generated policy from __boundari_tool_policy__.

from boundari import boundary_tool


@boundary_tool(
    name="email.send",
    risk="high",
    scopes=["email:send"],
    require_approval=True,
)
async def send_email(to: str, subject: str, body: str) -> dict[str, str]:
    return {"message_id": "msg_123", "status": "queued"}

YAML Policies

Create a policy file with:

boundari init

Example boundari.yaml:

agent: sales_ops_agent

budgets:
  max_tool_calls: 20
  max_runtime_seconds: 180
  max_cost_usd: "0.50"

tools:
  crm.read_contact:
    allow: true
    scopes: ["crm:read"]

  crm.update_contact:
    allow: true
    require_approval: true
    scopes: ["crm:write"]

  email.send:
    allow: true
    require_approval:
      when: "recipient_domain not in trusted_domains"
    input_schema: EmailInput
    output_schema: EmailResult

  shell.run:
    allow: false

data:
  redact:
    - api_key
    - credit_card
    - email
    - phone
  trusted_domains:
    - example.com

outputs:
  require_schema: true
  block_if_contains:
    - "{{SECRET}}"

policy_tests:
  forbidden_tools:
    - shell.run

Load it from Python:

from boundari import Boundary

boundary = Boundary.from_file("boundari.yaml")

Human Approval

Approval callbacks receive an ApprovalRequest with the exact tool name, argument summary, risk, reason, and run id.

from boundari import Boundary


def approve(request):
    return request.tool_name != "stripe.refund"


boundary = Boundary.from_file("boundari.yaml", approver=approve)

For local demos, pass console_approver:

from boundari import Boundary, console_approver

boundary = Boundary.from_file("boundari.yaml", approver=console_approver)

FastAPI users can build an approval route with fastapi_approval_router after installing boundari[web].

Redaction

Boundari redacts common sensitive values from tool outputs before returning them to the agent. Built-in rules include email addresses, phone numbers, credit-card-like numbers, and API keys.

from boundari import Redactor

redactor = Redactor(["email", "api_key"])
assert redactor.redact_text("alice@example.com sk-test-secret") == (
    "[REDACTED:email] [REDACTED:api_key]"
)

Raw tool inputs and outputs are not stored by default. JSONLAuditLog stores structured, redacted audit events.

CLI

boundari init
boundari validate boundari.yaml
boundari test boundari.yaml
boundari replay traces/run_123.jsonl --policy boundari.yaml
boundari explain traces/run_123.jsonl

boundari test fails when a policy is invalid, a configured forbidden tool is allowed, required output schema markers are missing, or a golden trace violates the contract.

Golden Traces

A replay trace is JSONL. Each line should include a tool name and optional args. Use expected_allowed when the trace should assert a specific outcome.

{"tool": "docs.search", "args": {"query": "refund policy"}, "expected_allowed": true}
{"tool": "shell.run", "args": {"command": "rm -rf /"}, "expected_allowed": false}

Framework Adapters

Install optional extras as adapters mature:

pip install "boundari[pydantic-ai]"
pip install "boundari[openai-agents]"

Current adapters keep the core package framework-agnostic and expose lightweight wrappers that preserve the original agent object while carrying the active boundary.

from boundari import Boundary
from boundari.adapters.pydantic_ai import wrap_agent

safe_agent = wrap_agent(agent, boundary=Boundary.from_file("boundari.yaml"))

Development

uv venv
uv sync --extra dev
uv run ruff check .
uv run mypy src
uv run pytest
uv run python -m build
uv run twine check dist/*

Security Positioning

Boundari reduces an agent's blast radius by enforcing deterministic runtime policy. It is not a prompt-injection scanner, hosted security product, model gateway, or secret manager. The core package does not make network calls.

License

MIT License. See LICENSE.

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

boundari-0.1.0.tar.gz (21.4 kB view details)

Uploaded Source

Built Distribution

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

boundari-0.1.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: boundari-0.1.0.tar.gz
  • Upload date:
  • Size: 21.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for boundari-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3483ebca8c4f37016b660abc25fe723cd24a150ccb6a47617adf1a8c15c6b6a6
MD5 f768dd1cb1ed42b003cfc4a4af7c28e5
BLAKE2b-256 f88d6f5fea394768580a8da3ae7989715b212fe56d88fa2d1436753e58101171

See more details on using hashes here.

File details

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

File metadata

  • Download URL: boundari-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for boundari-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d5037356e76d0e2a286054e76d32f875d5b34ab8ed8b770f97f8b67a17596445
MD5 abaf1f04580116602beefa13853036da
BLAKE2b-256 502fb5e537a1986bc17297a4da8dc26fd137b1e943ceaff5dbcb5ae61905f7ff

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