Skip to main content

AgentRails

Safety rails, a dry-run harness and an audit ledger for AI agents that take real, consequential actions — placing an order, spending an API budget, sending a message, running a command, changing infrastructure.

More people are wiring Claude (or another agent) directly to tools that do things, with almost nothing between "the agent decided" and "the action happened." AgentRails is that missing layer: a small, dependency-free Python library that sits between an agent's proposed actions and their execution.

What it is: a pure, unit-testable policy engine plus an audit ledger and a circuit breaker. What it is not: an agent, a strategy, or a decision-maker. It does not decide what to do. It decides what is allowed to be done, and it writes down everything that happens.

The pattern

AgentRails grew out of a live trading autopilot, but trading was only the domain where the problem showed up, not the problem. The reusable part is the pattern behind every high-stakes agent action:

declarative policy → validate the proposed action → dry-run → auditable ledger → circuit breaker after repeated failures.

Nothing about that is financial. It applies to any consequential action an autonomous agent might take. The lesson from running one in production: the safety logic has to be a pure function you can unit-test in milliseconds, completely separate from the code that actually acts — and it has to be re-checked against the final plan right before execution, not just trusted because the planner "should" have gotten it right.

Install

pip install -e .          # from a checkout, editable install
pip install -e ".[dev]"   # + pytest for running the test suite
pip install -e ".[mcp]"   # + the MCP server dependency (see below)

Requires Python 3.10+. (Not yet published to PyPI — this is a v0.1 scaffold.)

Quick example (generic core)

The core knows nothing about trading, money, or any domain. You describe the actions your agent wants to take, a policy says which are allowed, and a pure validator decides:

from agentrails import Action, ActionPlan, Policy, PolicyError, validate_actions

policy = Policy(
    scope_id="research-agent",
    allowed_targets={"anthropic", "openai"},  # deny-by-default
    dry_run=True,
    max_cost=1.00,             # per-action ceiling
    max_actions_per_run=20,
    budget=5.00,               # cap on the whole run
    human_approval_threshold=2.00,
)

# This came from YOUR agent, not from AgentRails.
plan = ActionPlan(
    scope_id="research-agent",
    generated_for="2026-07-18",
    dry_run=True,
    actions=[Action("call", target="anthropic", cost=0.40, reason="planning")],
)

try:
    validate_actions(plan, policy)
except PolicyError as e:
    print(f"Blocked: {e}")
    print(e.to_feedback_prompt())  # hand back to the planner to revise & retry
else:
    print("OK — allowed by policy.")

The generic guardrails

Policy fails closed by default: an empty allowed_targets denies every action and dry_run defaults to True, so an untouched policy can only ever simulate. validate_actions enforces:

  • allowed_targets — allowlist of what may be acted on (symbols, providers, recipients, commands, resources). Deny-by-default.
  • min_cost / max_cost — per-action size bounds on whatever "cost" means in your domain (dollars, recipients, tokens, files touched).
  • max_actions_per_run — cap on how many actions one plan may contain.
  • budget — the whole run's total cost can't exceed this (and, via PolicyContext, can't exceed what's actually available right now).
  • human_approval_threshold — actions above this raise a PolicyError flagged requires_human_approval=True, so your agent can pause for a person instead of executing.
  • max_target_concentration — rejects a plan that pushes more than a set share of the run onto a single target.
  • allow_irreversible — set False to block destructive/irreversible actions (delete, transfer, irreversible send) that carry reversible=False.
  • shadow_mode — instead of raising on the first violation, collect and return all violations without stopping. Use it to observe what the policy would have blocked before you enforce it.

When a plan is rejected, PolicyError.to_feedback_prompt() returns a structured message you can hand back to an LLM planner so it revises and retries against the same constraints.

Two cross-cutting pieces sit alongside the validator:

  • Ledger — an append-only CSV audit trail: every action ever proposed, dry-run, executed, rejected or skipped is a line in a file you can open in Excel, not a claim in a chat transcript. agentrails report ledger.csv turns it into a summary (proposed vs. blocked vs. executed, top targets); add --json for machine output.
  • CircuitBreaker — a file-backed pause switch that survives process restarts, so a scheduled agent that starts cold still remembers it's tripped after a bad run.

Reference adapters

An adapter maps a concrete domain onto the four core primitives (Action / ActionPlan / Policy / PolicyContext). Three ship today, on purpose from very different domains, to prove the core is genuinely generic and not just trading with new labels:

agentrails.adapters.trading

Caps a broker trade plan (Robinhood, Alpaca, IBKR, ...). Cost axis = dollars per order. Its GuardrailConfig / TradePlan / validate_plan API is the original, still-canonical trading validator; it also enforces two genuinely trading-specific rules the generic core doesn't know about — a mandatory stop-loss and "can't sell what you don't hold." See examples/weekly_run_example.py.

agentrails.adapters.api_spend

Caps how much an autonomous agent can spend calling paid APIs (LLM providers, search, ...). Cost axis = dollars per call. ApiCall / ApiCallPlan / SpendPolicy / SpendState map onto the same core, and validate_api_spend() enforces provider allowlist, per-call ceiling, calls-per-run, per-run and daily budget, human-approval threshold and provider concentration — all from the generic core — plus one API-specific rule (an optional per-model allowlist) kept in the adapter. See examples/api_spend_example.py.

agentrails.adapters.shell

Gates command execution — the scariest thing people wire agents to, and the adapter that exercises the core's irreversibility primitive. Here "cost" is barely used; the star is reversible=False. CommandRequest / CommandPlan / ShellPolicy map onto the core (executable allowlist, commands-per-run), and validate_commands() adds two command-specific rules: a destructive-command heuristic that flags rm -rf, dd, git push --force, DROP TABLE, fork bombs (blocked unless allow_destructive=True), and a shell-operator guard that rejects ;, |, `, $( ) and redirects so a second command can't be smuggled past the allowlist. It's a tripwire, not a sandbox — pair it with real OS-level isolation. See examples/shell_guard_example.py.

Write your own adapter

The recipe is always the same: translate your domain into core primitives, let validate_actions do the generic work, and keep whatever rule is genuinely domain-specific in your own thin validator. A whole adapter is ~30 lines:

from dataclasses import dataclass, field
from agentrails import Action, ActionPlan, Policy, PolicyContext, validate_actions

@dataclass
class EmailPolicy:
    scope_id: str
    allowed_domains: set[str] = field(default_factory=set)
    max_recipients_per_send: int | None = None   # -> Policy.max_cost
    daily_recipient_cap: int | None = None        # -> PolicyContext.available_budget

def email_to_action(msg) -> Action:
    # cost axis here isn't money — it's how many people this reaches
    return Action("send", target=msg.domain, cost=len(msg.recipients))

def policy_to_core(p: EmailPolicy) -> Policy:
    return Policy(
        scope_id=p.scope_id,
        allowed_targets=set(p.allowed_domains),
        max_cost=p.max_recipients_per_send,
        budget=p.daily_recipient_cap,
    )

That cost = len(recipients) line is the whole point: "cost" is any axis your policy limits, not just dollars. The three shipped adapters are worked examples of this exact recipe.

Declarative policies

Keep the rules in a config file, reviewed like code and diffable, instead of buried in Python. JSON works out of the box; YAML needs pip install agentrails[yaml], so the library stays dependency-free by default.

from agentrails import load_policy, validate_actions

policy = load_policy("policy.json")   # or policy.yaml / policy.yml
validate_actions(plan, policy)
{ "scope_id": "research-agent", "allowed_targets": ["anthropic", "openai"],
  "dry_run": false, "max_cost": 1.0, "budget": 5.0, "human_approval_threshold": 2.0 }

Loading fails closed on typos: an unknown key (budjet, max_costt) raises rather than being silently ignored, so a misspelled limit can't quietly leave you unprotected. save_policy / policy_to_dict do the reverse (sets serialize as sorted lists for stable diffs).

One-line integration

@guarded wraps your executor so the whole pass — circuit breaker → policy validation → audit ledger → execute — happens on every call, with the body only running if the plan is allowed:

from agentrails import guarded, load_policy, Ledger, CircuitBreaker

@guarded(load_policy("policy.json"), ledger=Ledger("audit.csv"), breaker=CircuitBreaker("cb.json"))
def call_apis(plan):
    ...  # only runs if the plan passes; skipped entirely in dry-run

call_apis(plan)   # raises PolicyError / CircuitBreakerTripped instead of acting

Prefer to stay explicit? Guard(policy, ledger=…, breaker=…) exposes authorize(plan) (raise-or-pass) and run(plan, execute) (the full flow) as plain methods. See examples/guarded_tool_example.py — it's runnable as-is.

Optional MCP gateway

src/agentrails/mcp_server.py is a reference FastMCP server exposing two tools an agent can call before it acts:

  • evaluate_actions — the generic one: propose any actions (API calls, messages, commands, ...), AgentRails runs the full pass (circuit breaker → validate_actions → ledger) and returns a success, a human-approval hold, or the feedback prompt above.
  • evaluate_and_place_trade — the trading adapter's tool, kept as a worked example of a domain-specific gateway on top of the same pass.

It ships with mock demo config and requires the optional mcp dependency (pip install -e ".[mcp]") only to serve; the module still imports without it (a shim stands in for FastMCP) so the evaluation logic stays unit-testable. It's an example wiring, not a hosted service — nothing in it talks to a real broker or any other live system.

What's deliberately NOT in scope

  • No agent, no strategy, no decision logic. What to do is the one part that should stay yours — your valuation model, your retrieval logic, your ops runbook. AgentRails' job starts the moment you have a proposed plan.
  • No SDK/tool client. AgentRails never makes the call itself: you call your broker MCP / API client / shell, AgentRails tells you whether you're allowed to. That's what keeps it domain- and vendor-agnostic.
  • No hosted dashboard, no held credentials. Nothing here talks to the network. Your keys, your accounts, your machine.

Secrets

AgentRails itself never touches the network and holds no credentials. When you wire it to a broker, an API provider or a notifier (Telegram, etc.), keep those keys in environment variables or a local .env file — never in code or in the repo. .env is gitignored; copy .env.example to .env and fill in your own values.

Legal

This is developer tooling for people automating their own actions with their own credentials. It enforces the boundaries you configure; it does not eliminate the risk of the underlying action.

The trading adapter specifically: it is not investment advice, not a signal service, and not a recommendation to buy or sell anything. If you're building something that places trades on behalf of other people's accounts for compensation, that's regulated activity in most jurisdictions (investment adviser / broker-dealer registration in the US, equivalents elsewhere) — talk to a securities attorney before you monetize in that direction. Automating your own account carries the same market risk as any other trading; nothing here removes it.

Status

v0.1 — the generic core (allowlist, size and budget limits, human-approval threshold, concentration, irreversibility, shadow mode), the ledger, the circuit breaker, declarative policies (JSON/YAML), the @guarded wrapper, the agentrails report CLI, a reference MCP gateway, and three reference adapters (trading, api_spend, shell) are implemented and tested (133 passing tests, CI on Python 3.10–3.13). See SECURITY.md for the trust boundary. Not yet published to PyPI.

About

AgentRails is built and maintained by Kratos Analytics LLC, a quantitative data analytics and software development firm specializing in big data solutions and algorithmic trading engines. It's released as open-source, MIT-licensed infrastructure — not a paid product — because the pattern is worth having in the open.

If you're building AI agents that need real guardrails in production (trading, ops automation, API-spend control, or your own domain) and want help designing or hardening the policy layer, reach out: kratosanalyticsllc@gmail.com.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kratos_agentrails-0.1.0.tar.gz (53.8 kB view details)

Uploaded Source

Built Distribution

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

kratos_agentrails-0.1.0-py3-none-any.whl (39.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: kratos_agentrails-0.1.0.tar.gz
  • Upload date:
  • Size: 53.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for kratos_agentrails-0.1.0.tar.gz
Algorithm Hash digest
SHA256 249ac452641c1d97526cedc61e0d345b2e7a263614170bfe78753d5fe0eeb21d
MD5 c5c848e145149f3cb56eb06aad92ba4d
BLAKE2b-256 a0f6ec92bc70da6d880cc1d373aa8b5d7092af26f4f03f45c0a7137366fec6eb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for kratos_agentrails-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 09103d7dde5cddff5ccc8f3234f93a157b6163480dfd988521744b2d762aac21
MD5 c1902966f47319369234fa6b31923760
BLAKE2b-256 66944c4fc3978a9deae983d2ed1575ee5b3386d2f677c1669716f7d865bb7260

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 Sentry Error logging StatusPage Status page