Why
AI agents are shipping fast and getting real permissions — deleting records, sending emails, moving money, running shell commands. Most teams have zero enforcement between "the model decided to call a tool" and "the tool ran." bastion sits in that gap:
agent decides to call a tool
│
▼
┌──────────┐ allow ──▶ tool runs
│ bastion │ block ──▶ denied, agent sees why
│ policy │ approve ──▶ human reviews (Slack / dashboard / console)
└──────────┘
│
▼
every decision logged
Not another prompt-injection classifier — enforcement on what agents are actually allowed to do.
What you get
| 🛡️ Policy engine | Allow / block / require-approval rules, glob-matched tool names, conditions on args (amount > 500, regex match, contains-PII, ...) |
| ✅ Human-in-the-loop | Console prompt, Slack (reaction-based, no webhook server), or the hosted dashboard — pluggable |
| 📝 Audit log, for free | Every decision recorded automatically — JSONL locally or shipped to the dashboard |
| 🔍 PII/secrets scanning | Regex-based (no ML model to download) — emails, SSNs, Luhn-validated credit cards, cloud credentials, private keys |
| 📄 Declarative policy | Rules in YAML, editable by a non-engineer reviewer — by hand or from the dashboard's /policy page |
| 🔌 3 framework integrations | LangGraph, OpenAI Agents SDK, Claude Agent SDK — one line to wrap your tools |
| 📊 Dashboard | Audit log, approval queue, and a policy editor — a real Next.js app |
Quickstart
pip install bastionguard— the PyPI distribution is namedbastionguard(plainbastionwas already taken — an old Python 2 stdlib module, of all things). The import name is unaffected: stillimport bastion.
from bastion import PolicyEngine, Rule, Action, guard
from bastion.policy import arg_exceeds
policy = PolicyEngine()
policy.add_rule(Rule(tool_pattern="delete_*", action=Action.BLOCK, reason="irreversible"))
policy.add_rule(Rule(
tool_pattern="transfer_funds",
action=Action.APPROVE,
condition=arg_exceeds("amount", 500),
reason="large transfers need a human",
))
def transfer_funds(account: str, amount: float) -> str:
return f"sent ${amount} to {account}"
guarded = guard(transfer_funds, policy=policy)
guarded(account="acct_1", amount=600) # prompts for approval in the terminal
python examples/basic_example.py # runs the full demo above
Rules can also live in YAML instead of hand-written Python:
from bastion import load_policy_from_yaml
policy = load_policy_from_yaml("policy.yaml")
See examples/policy.yaml for the schema.
Framework integrations
LangGraph
from bastion.integrations.langgraph import guarded_tool_node
tool_node = guarded_tool_node([my_tool_a, my_tool_b], policy=policy)
# use tool_node exactly where you'd use langgraph.prebuilt.ToolNode(tools)
A blocked or approval-denied call comes back as a normal error ToolMessage
(handle_tool_errors=BlockedByPolicy), so the agent can react to it instead
of the graph run crashing. See examples/langgraph_demo.py.
pip install "bastionguard[langgraph]"
OpenAI Agents SDK
from bastion.integrations.openai_agents import guarded_tools
agent = Agent(name="...", tools=guarded_tools([my_tool_a, my_tool_b], policy=policy))
A blocked or approval-denied call returns a descriptive string as the
tool's result ("Tool call blocked by policy: <reason>") rather than
raising — the same idiom the SDK itself uses when a tool raises an
exception, so the model sees why and can react. See
examples/openai_agents_demo.py.
pip install "bastionguard[openai-agents]"
Claude Agent SDK
from claude_agent_sdk import ClaudeAgentOptions
from bastion.integrations.claude_agent_sdk import guarded_can_use_tool
options = ClaudeAgentOptions(can_use_tool=guarded_can_use_tool(policy=policy))
Hooks into the SDK's own permission system (can_use_tool), which it calls
for every tool invocation — built-in tools (Bash, Read, Write, ...) and
custom tools registered via create_sdk_mcp_server alike. Unlike the other
two integrations, nothing needs wrapping per-tool — one callback covers
everything. See examples/claude_agent_sdk_demo.py.
pip install "bastionguard[claude-agent-sdk]"
PII / secrets scanning
from bastion import Rule, Action, contains_pii, enforce_text_policy
policy.add_rule(Rule(
tool_pattern="*",
action=Action.BLOCK,
condition=contains_pii(["ssn_us", "credit_card"]),
reason="tool call args contain PII/secrets",
))
# directly on an LLM response, outside the tool-call path:
enforce_text_policy(llm_output, categories=["email"], on_detect="redact")
Also available as a YAML condition type (type: contains_pii). See
examples/pii_scanning_example.py.
Slack approval
from bastion.integrations.slack import SlackApprovalHandler
approval_handler = SlackApprovalHandler(
token="xoxb-...", # needs chat:write + reactions:read scopes
channel="#agent-approvals",
)
Posts a message and polls for a ✅/❌ reaction — no webhook server required.
Fails closed (denies) if nobody responds within timeout seconds.
Dashboard
A real Next.js app (dashboard/) — audit log, approval queue, and a
policy editor, all wired to the SDK over HTTP:
from bastion import AuditLogger, guard
from bastion.integrations.dashboard import (
DashboardApprovalHandler,
dashboard_audit_sink,
load_policy_from_dashboard,
)
api_key = "..." # must match DASHBOARD_API_KEY in the dashboard's env
policy = load_policy_from_dashboard("http://localhost:3000", api_key)
audit = AuditLogger(sink=dashboard_audit_sink("http://localhost:3000", api_key))
approval_handler = DashboardApprovalHandler("http://localhost:3000", api_key)
The dashboard UI is behind a separate session login (not this API key) —
see dashboard/README.md for the full auth model
and self-hosting setup.
Install
pip install bastionguard
# with a framework integration:
pip install "bastionguard[langgraph]" # or [openai-agents] / [claude-agent-sdk]
# with Slack approval:
pip install "bastionguard[slack]"
For local development (editable install, running the test suite), see CONTRIBUTING.md.
Tests
ruff check . && mypy src && pytest -q
Framework integration tests run against the real installed package for each framework, never a mock — see AGENTS.md for why that's a hard rule here.
Project status
Pre-1.0. Core SDK, all three framework integrations, and the dashboard are built and tested — see CHANGELOG.md for what's shipped and PLAN.md for what's next. Not yet on PyPI; install from source for now (see CONTRIBUTING.md).
Contributing
Issues and PRs welcome — see CONTRIBUTING.md. Found a security issue? See SECURITY.md instead of opening a public issue for it.
License
Metadata
Release files for bastionguard 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bastionguard-0.1.0.tar.gz | 30.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bastionguard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.8 kB
Release files / bastionguard-0.1.0.tar.gz
| Download URL | bastionguard-0.1.0.tar.gz |
|---|---|
| Size | 30.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0219262a8a4affc45cf6a9771a11487bcc49fb00de82528d9f53a1782f804c46
|
|
BLAKE2b-256 checksum How to use checksums |
e459fe345c006688349bfd1fbe859d2264e7bce119644b35a33e46f0409e690e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / bastionguard-0.1.0-py3-none-any.whl
| Download URL | bastionguard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0f8414638dadbd5d1a911898a9eb0d4f496b5846cf5c427846e5915b04c8b32b
|
|
BLAKE2b-256 checksum How to use checksums |
4eff77719c8eda7046650cf5eeae25670aea1adab89a99e9912e4c89ad3a7d91
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log