bulwark-sdk (Python)
Governance, audit and policy decisions for AI agents.
Zero runtime dependencies — stdlib only. Python 3.9+.
Install
Not yet on PyPI. Install from git:
pip install bulwark-sdk
If that fails with "No matching distribution found", the package is not on PyPI yet. Two alternatives, both fully supported:
- Use the REST API directly. Three calls, no install. See
docs/API.md§2. - Vendor it. Copy
src/bulwark/into your project. Zero dependencies, nothing to install.
The one thing to understand first
There are two calls, and they do different jobs:
| Call | Endpoint | What happens | Counts as a decision? |
|---|---|---|---|
observe() |
POST /v1/events |
Appended to the audit chain. No policy is evaluated. Buffered + batched. | No |
decide() |
POST /v1/decide |
Policy is evaluated. Returns allow/deny/escalate, can raise a human approval. | Yes |
If you only call observe(), your dashboard shows activity but zero decisions, zero flags, and a
risk score driven only by registry facts. That is working as designed. Call decide() at the point
your agent is about to do something that matters.
Bulwark never blocks your agent. decide() returns a verdict; acting on it is your code's job.
Quickstart
from bulwark import BulwarkClient
bw = BulwarkClient(api_key="bw_live_...") # Settings → API keys
# 1. Register once (at deploy time, not per request)
agent = bw.register_agent(
name="Portfolio Assistant",
purpose="Answers investor questions about portfolio performance",
risk_tier="medium",
environment="prod",
tools=["portfolio.read", "ai.assistant.answer"],
)
bw.activate_agent(agent["agentId"]) # agents are created "pending"
# 2. Record what the agent did
bw.observe(agent["agentId"], "ai.assistant.answer", resource="portfolio")
# 3. Ask for a verdict before something consequential
verdict = bw.decide(agent["agentId"], "crm.write", resource="customers")
if verdict.pending:
log.warning("Awaiting human approval: %s", verdict.approval_id)
elif not verdict.allowed:
log.warning("Bulwark would block this: %s", verdict.reason)
Three things that surprise people
1. Agents start pending, not active. decide() denies anything from a non-active agent with
agent_not_active. Call activate_agent() after registering.
2. tools is currently descriptive, not enforced. Declaring tools=["crm.read"] records the
agent's intended allowlist and drives the access-matrix view, but Bulwark does not yet reject or
escalate an action outside it. Today this returns allow:
bw.register_agent(..., tools=["crm.read"])
bw.decide(agent_id, "crm.write") # -> allow, "no matching policy — default allow"
To enforce scope today, author an explicit policy (POST /v1/policies) with a rule matching the
actions you want escalated. Declare tools anyway — it is the allowlist scope enforcement will read
once it ships.
3. Events for an unregistered agent are dead-lettered, not rejected. A typo'd agent_id fails
quietly — the event lands in /v1/events/dead-letter rather than erroring. The SDK emits a warning
when the API reports dead-lettered events.
Serverless
The background flusher does not run once your process is frozen or torn down. Flush before you return:
def handler(event, context):
bw.observe(agent_id, "doc.process", resource=event["key"])
...
bw.flush() # or: with BulwarkClient(...) as bw:
return {"ok": True}
flush() guarantees that everything buffered when you called it has been sent before it returns.
Failure behaviour
fail_open=True (the default) is the monitor-mode contract: governance being down must never take
your agent down.
observe()never raises. Failed batches are dropped and counted inbw.dropped_events.decide()returns an allow markeddegraded=True. Never treat a degraded verdict as a real governance record — it means no policy was evaluated.
Set fail_open=False to surface errors as exceptions instead.
Don't send content
Never pass prompts, completions, or document bodies in context. Bulwark strips content fields at
ingest by design, but the cheapest data to protect is the data you never transmit. Send metadata:
identifiers, action names, resource names, counts.
Configuration
BulwarkClient(
api_key,
base_url="https://bulwark-api.fly.dev",
timeout=5.0,
batch_size=100, # API hard cap is 100 events per batch
flush_interval=2.0, # background flush cadence, seconds
fail_open=True,
max_queue=10_000, # buffer bound; oldest dropped beyond it
max_retries=3, # 429 and 5xx only, honours retry-after
)
API
| Method | Purpose |
|---|---|
register_agent(name, purpose, risk_tier, environment, *, tools, data_sources, models_used, scope, category, owner_user_id, spend_limit_gbp) |
Register. Returns the agent dict (state pending). |
activate_agent(agent_id) |
Transition to active. |
set_agent_state(agent_id, state) |
draft→pending→active→suspended/retired. |
list_agents() |
All agents for your tenant. |
observe(agent_id, action, *, resource, on_behalf_of, context, type) |
Buffered telemetry. |
decide(agent_id, action_type, *, resource, data_classification, context) |
Policy verdict. |
flush(timeout=None) |
Send everything buffered. |
close() |
Flush and stop the worker. Runs at exit automatically. |
Exceptions
All inherit BulwarkError. BulwarkAuthError (401), BulwarkPermissionError (403),
BulwarkUpgradeRequired (402, has .feature), BulwarkRateLimited (429, has .retry_after),
BulwarkPayloadTooLarge (413), BulwarkValidationError (400, .body["details"]),
BulwarkTransportError (network).
An API key maps to the developer role: it can register agents, send events and call decide(), but
cannot mint keys, manage team members, or change the plan — those need an admin session.
Tests
pip install -e ".[dev]"
python -m pytest
Metadata
Release files for bulwark-sdk 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 | Provenance | |
|---|---|---|---|---|
| bulwark_sdk-0.1.0.tar.gz | 17.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bulwark_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.3 kB
Release files / bulwark_sdk-0.1.0.tar.gz
| Download URL | bulwark_sdk-0.1.0.tar.gz |
|---|---|
| Size | 17.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e3e1aa9b28b985c5489a69c1c5067edaa9db868550a526bc11470ed2e887203c
|
|
BLAKE2b-256 checksum How to use checksums |
14632bbb3616987f523aaf44b6718185082e0b11be36569d929f511f7d935fe3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / bulwark_sdk-0.1.0-py3-none-any.whl
| Download URL | bulwark_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
87d6216b68b19fbc59a871dad2e7551c255996b49d2d89dea67cd52927bd64cd
|
|
BLAKE2b-256 checksum How to use checksums |
5d7343340b5220a941584c49c06d0b112a7c2629adda8c68d950f6d3415ea2df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Security for bulwark-sdk 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.