Guardrails and observability for autonomous AI agents. Tool-call auditing, spend caps, and rollback for anything an agent does in production.
pip install agentrail-sdk
agentrail init- Why This Exists
- Why Three Languages
- Install
- Wrapping Your Agent's Tools
- CLI
- Repo Layout
- Configuration
- Performance
- CI
- Known Limitations
- Contributing
- License
Agent frameworks are good at getting an LLM to call tools. None of them are good at answering "what did it actually do, why, and how do I undo it" after the fact.
Give an agent a database write tool, a payment API, or git push access, and the failure mode isn't "it hallucinated an answer" β it's "it did something real, silently."
AgentRail sits between your agent and the tools it calls, and gives you three things most setups don't have:
| π Hard stop | Blocks expensive or destructive actions before they execute |
| π Audit trail | Complete record of every tool call, cost, and decision |
| β©οΈ Rollback | A way to undo what happened |
This isn't polyglot for its own sake β each piece is written in what it's actually good at, and the split is load-bearing.
Every tool call an agent makes blocks on a policy check. That check needs to happen in single-digit milliseconds regardless of how many agents are hammering it concurrently. Go gives us a static binary with no runtime dependency, cheap goroutines for concurrent session handling, and GC pauses low enough not to show up in the latency budget.
Every agent framework worth integrating with (LangChain, CrewAI, raw OpenAI/Anthropic function-calling loops) lives in Python. The SDK has to feel native there, so it is. It talks to the Go daemon over a local Unix socket via gRPC β the interception logic itself is thin; the daemon does the real work.
The one piece humans actually look at. Approval queues and audit trails are a UI problem, and the web ecosystem is where that problem is best solved. Talks to the daemon over gRPC-web.
Net effect: sub-millisecond policy checks even under load, an SDK that doesn't feel bolted-on in whatever Python agent code you're already writing, and a dashboard that doesn't feel like an afterthought.
βββββββββββββββββββββββ Unix socket / gRPC ββββββββββββββββββββββββ
β Python SDK β βββββββββββββββββββββββββββββββββΊβ Go Policy Daemon β
β (@guard decorator) β ββββββββββββββββββββββββββββββββββ (agentraild) β
βββββββββββββββββββββββ βββββββββββββ¬βββββββββββ
β
βββββββββββΌβββββββββββ
β Audit Store β
β (Postgres / SQLite) β
βββββββββββ¬βββββββββββ
β gRPC-web
βββββββββββΌβββββββββββ
β TS Dashboard β
β (approvals, trace) β
ββββββββββββββββββββββββ
Three components β install what you need.
Python SDK (always required β this is what your agent code imports)
pip install agentrail-sdkGo daemon (runs locally or as a sidecar)
brew install yourorg/tap/agentraild
# or
go install github.com/yourorg/agentrail/daemon/cmd/agentraild@latestDashboard (optional β only needed for the approval UI)
docker compose -f docker/dashboard-compose.yml up -d
agentrail initscaffolds config and starts the daemon in dev mode (in-process, no separate binary needed) so you can try it without standing up all three pieces.
from agentrail import guard, Policy
policy = Policy(
max_session_spend_usd=5.00,
require_approval=["send_email", "execute_sql"],
block=["DROP TABLE", "rm -rf"],
)
@guard(policy, reversible=True, undo="delete_row")
def insert_row(table: str, data: dict):
return db.insert(table, data)Under the hood, @guard makes a ~0.4ms round trip to agentraild over the local socket before letting the call through.
Framework adapters ship for LangChain and native OpenAI/Anthropic function-calling:
from agentrail.adapters.langchain import wrap_tools
tools = wrap_tools(my_langchain_tools, policy=policy)agentrail watch # tail audit log live
agentrail trace abc123 # what did session abc123 do
agentrail rollback abc123 --confirm # undo everything reversible
agentrail spend --agent research-bot --window 24hExample trace:
$ agentrail trace abc123
Session abc123 β agent: support-bot β started 2026-08-26 09:14:02
[09:14:03] search_knowledge_base("refund policy") $0.0002 ok
[09:14:05] check_order_status(order_id="ORD-9921") $0.0001 ok
[09:14:08] BLOCKED issue_refund(order_id="ORD-9921", amount=450.00)
reason: exceeds require_approval threshold ($50)
β escalated to approval dashboard
Total spend: $0.0003 | 1 blocked | 0 rolled back
.
βββ sdk-python/ # the pip-installable package agents import
β βββ agentrail/
β β βββ core/
β β β βββ guard.py # decorator, calls daemon via gRPC
β β β βββ client.py # gRPC client to agentraild
β β βββ adapters/
β β β βββ langchain.py
β β β βββ openai_functions.py
β β βββ cli.py
β βββ tests/
β βββ pyproject.toml
β
βββ daemon/ # agentraild β the Go policy engine
β βββ cmd/agentraild/
β β βββ main.go
β βββ internal/
β β βββ policy/ # rule evaluation, spend tracking
β β βββ storage/ # Postgres/SQLite backends
β β βββ rollback/
β β βββ anomaly/ # baseline + deviation scoring
β βββ grpc/ # service definitions, generated code
β βββ go.mod
β βββ Makefile
β
βββ dashboard/ # TS/Next.js approval queue + audit browser
β βββ src/
β β βββ app/
β β βββ components/
β β βββ lib/grpc-web-client.ts
β βββ package.json
β βββ next.config.js
β
βββ proto/ # shared .proto definitions (SDK β daemon β dashboard)
β βββ agentrail/v1/policy.proto
β
βββ examples/
β βββ langchain_support_bot/
β βββ raw_function_calling/
β
βββ docs/
β βββ writing-a-policy.md
β βββ rollback-guide.md
β βββ daemon-deployment.md
β βββ architecture.md
β
βββ docker/
β βββ daemon-compose.yml
β βββ dashboard-compose.yml
β
βββ .github/
β βββ workflows/
β β βββ ci-python.yml
β β βββ ci-go.yml
β β βββ ci-dashboard.yml
β βββ ISSUE_TEMPLATE/
β βββ PULL_REQUEST_TEMPLATE.md
β
βββ .editorconfig
βββ .gitignore
βββ CODEOWNERS
βββ CONTRIBUTING.md
βββ CHANGELOG.md
βββ SECURITY.md
βββ LICENSE
βββ README.md
agentrail.yaml β read by the Go daemon on startup:
storage:
backend: postgres
connection: "${AGENTRAIL_DB_URL}"
policy:
max_session_spend_usd: 5.00
max_daily_spend_usd: 100.00
require_approval: [send_email, execute_sql, issue_refund]
block_patterns: ["DROP TABLE", "rm -rf"]
allowlist_mode: false
approval:
channel: slack
webhook_url: "${APPROVAL_WEBHOOK}"
timeout_seconds: 300
on_timeout: deny
anomaly_detection:
enabled: true
baseline_window_days: 14Benchmarked with the Go daemon running as a local sidecar, 10,000 tool calls, Unix socket transport:
| Configuration | Added latency per call |
|---|---|
| Policy check only | ~0.4ms |
| Policy + audit log write (Postgres) | ~1.3ms |
| Policy + audit + anomaly scoring | ~2.1ms |
For comparison, an earlier in-process Python-only prototype averaged ~4ms for the same policy+audit path β the daemon split was worth doing specifically because agent loops sometimes fire several tool calls per turn, and that adds up under concurrency.
Each component has its own workflow and only runs on changes to its directory:
# .github/workflows/ci-go.yml β runs on daemon/** changes
- run: go test ./... -race
- run: golangci-lint run
# .github/workflows/ci-python.yml β runs on sdk-python/** changes
- run: pytest --cov=agentrail
# .github/workflows/ci-dashboard.yml β runs on dashboard/** changes
- run: npm run lint && npm run buildproto/ changes trigger all three CI workflows, since a schema change touches every component.
- The daemon must be running for the SDK to work in production mode β
agentrail init's dev mode (in-process, no daemon) is for local testing only, not deployment. - Anomaly detection needs ~2 weeks of baseline data before it's useful β it's silent-only until then.
- Rollback is only as good as the
undofunction you write β AgentRail verifies that it ran, not that it's correct.
Each directory is closer to its own project than a monorepo convenience β sdk-python/, daemon/, and dashboard/ each have their own CONTRIBUTING notes linked from the root CONTRIBUTING.md.
Proto changes need sign-off from a CODEOWNERS reviewer on all three sides, since they're a breaking-change surface by nature.
MIT. See LICENSE.
AgentRail limits blast radius, it doesn't guarantee correctness. This is the seatbelt, not the driver.
agentrail: connection refused- the daemon is not running. Start it withairecalld --config agentrail.yamland retry.- Everything is blocked - check
max_daily_spend_usd; a crossed daily cap blocks all calls until midnight UTC.