bastionprobe
Pentest for AI agents. Fire indirect prompt-injection payloads at your agent and get a report of which ones land. Every payload that gets through is a hole a runtime guard should close.
The offensive twin of agentbastion (the runtime firewall). agentbastion blocks attacks in production; bastionprobe launches them before you ship. Scan with bastionprobe → find holes → plug them with agentbastion → re-scan → green.
bastionprobe = Burp Suite for agents (red team, pre-ship, on demand)
agentbastion = WAF for agents (blue team, production, always on)
The wedge: indirect prompt injection
The hardest injection to defend and the one most agents are blind to: the payload doesn't come from the user — it rides inside a tool result the agent reads mid-task (a fetched web page, a support ticket, a DB row, a file). If the agent treats that data as instructions, you're owned.
bastionprobe plants a payload in a tool output, runs your agent for one turn, and scores whether it obeyed:
- canary check — the payload tells the agent to emit a unique random token. Token shows up in the reply ⇒ injection landed. (Fresh token per run — can't be faked or pre-trained.)
- tool check — the payload tells the agent to call a forbidden tool
(
send_email,delete_file,http_post, …). Agent calls it ⇒ landed.
Install
pip install bastionprobe
Quick start
Zero setup — fire at the bundled demo targets:
bastionprobe run # vulnerable demo: 12/12 land
bastionprobe run --target bastionprobe.demo:safe_agent # safe demo: 0 land
Fire at your agent — implement one function (see
examples/my_agent.py):
from bastionprobe import AgentResponse
def agent(messages, tool_outputs): # tool_outputs = {tool_name: output}
reply, tools_called = run_your_agent(messages, tool_outputs)
return AgentResponse(text=reply, tool_calls=tools_called)
bastionprobe run --target examples.my_agent:agent --out results.jsonl
Exit code is non-zero when any payload lands, so you can gate CI on it:
bastionprobe run --target examples.my_agent:agent || echo "agent is vulnerable"
Fire at a real Claude agent
A built-in adapter turns an Anthropic client into a target. It stands up a small
tool-using agent, feeds each payload back as the result of the agent's
read_document tool, runs one real model turn, and reports what Claude did —
text plus any tools it tried to call.
pip install "bastionprobe[anthropic]"
export ANTHROPIC_API_KEY=...
from anthropic import Anthropic
from bastionprobe import load_payloads, run_suite, make_anthropic_target
from bastionprobe.report import render
target = make_anthropic_target(Anthropic(), model="claude-sonnet-4-5")
print(render(run_suite(target, load_payloads())))
Set model= to the model your production agent runs — that's the behavior you
care about. system= and tools= are overridable so you can mirror your real
agent's persona and toolbox instead of the defaults. See
examples/anthropic_scan.py.
Close the loop: harden the shield
The sword's whole point is to make the shield better. harden turns landed
findings into defenses agentbastion
loads directly:
bastionprobe run --target examples.my_agent:agent --out results.jsonl
bastionprobe harden results.jsonl # -> bastion_hardening/{policy.yaml, injections.jsonl}
policy.yaml— every tool an injection got to call, as a deny-list for agentbastion'sToolPolicy(load_policy).injections.jsonl— every injection string that worked, in agentbastion's corpus schema. Load them asSemanticDetectortemplates and embedding similarity blocks those attacks and their paraphrases.
from agentbastion import Firewall, load_policy
from agentbastion.semantic import SemanticDetector
from agentbastion.inbound import InboundGuard
import json
templates = [json.loads(l)["text"] for l in open("bastion_hardening/injections.jsonl")]
fw = Firewall()
fw.tool_policy = load_policy("bastion_hardening/policy.yaml") # deny what got called
fw.inbound = InboundGuard(detectors=[SemanticDetector(embed_fn, templates=templates)])
Then re-scan: scan → harden → load → re-scan, and each round the shield learns exactly what the sword got through.
The target contract
A target is any callable (messages, tool_outputs) -> AgentResponse. bastionprobe
poisons one value in tool_outputs, runs your agent for one turn, and reads
back the reply text plus the names of any tools it called. It never looks inside
the agent — it measures behavior. Report what your agent actually did.
Shared corpus with agentbastion
Payloads use the same category taxonomy as agentbastion's
benchmark/corpus.jsonl (indirect_injection, exfiltration,
direct_injection, …). Same strings, opposite direction: agentbastion reads a
row as "block this", bastionprobe reads it as "fire this". A finding here maps
directly to a rule there.
Status
Alpha. One attack class (indirect injection via tool output), 14 payloads across 5 languages, tuned against a live model (benign-operational framing lands where explicit adversarial phrasing gets flagged). Reserved for later: direct injection, RAG poisoning, multi-agent trust escalation, LLM-judge scoring. Payload PRs welcome.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bastionprobe-0.4.0.tar.gz.
File metadata
- Download URL: bastionprobe-0.4.0.tar.gz
- Upload date:
- Size: 17.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c0a379f07418cc7d36427c705a36e71b20d2c84c1f06e0a85fd720604382bf3
|
|
| MD5 |
75af41d45b9cf543cf1d8281e41d472e
|
|
| BLAKE2b-256 |
3fa0888aff932b2b4f607143b469dd611bf9273328558ff6ecde4fb567f60f9f
|
Provenance
The following attestation bundles were made for bastionprobe-0.4.0.tar.gz:
Publisher:
publish.yml on Rinkia/bastionprobe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bastionprobe-0.4.0.tar.gz -
Subject digest:
3c0a379f07418cc7d36427c705a36e71b20d2c84c1f06e0a85fd720604382bf3 - Sigstore transparency entry: 2759665616
- Sigstore integration time:
-
Permalink:
Rinkia/bastionprobe@b439fac3e3e97de2f779ea979e46c83ac802ec87 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Rinkia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b439fac3e3e97de2f779ea979e46c83ac802ec87 -
Trigger Event:
release
-
Statement type:
File details
Details for the file bastionprobe-0.4.0-py3-none-any.whl.
File metadata
- Download URL: bastionprobe-0.4.0-py3-none-any.whl
- Upload date:
- Size: 17.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e85a41500f7fdc309fefcba809bf3024db48a4c075a617c80a27727d4df4446
|
|
| MD5 |
6a93865731765c0447cf1e605babefc7
|
|
| BLAKE2b-256 |
a676f012641d3020bdece7b4ce38ae7b0119f3dc9093021a8e4e0f8e77dbfb48
|
Provenance
The following attestation bundles were made for bastionprobe-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on Rinkia/bastionprobe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bastionprobe-0.4.0-py3-none-any.whl -
Subject digest:
2e85a41500f7fdc309fefcba809bf3024db48a4c075a617c80a27727d4df4446 - Sigstore transparency entry: 2759665791
- Sigstore integration time:
-
Permalink:
Rinkia/bastionprobe@b439fac3e3e97de2f779ea979e46c83ac802ec87 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/Rinkia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b439fac3e3e97de2f779ea979e46c83ac802ec87 -
Trigger Event:
release
-
Statement type: