agent-boundary-scan
Static analysis that detects confused-deputy vulnerabilities in multi-agent and MCP-based AI systems — unvalidated paths where attacker-influenced data can reach a privileged action (shell exec, file write, credential access, network egress, agent delegation) with no trust boundary in between. It reads an agent/tool graph and reports where the unvalidated paths are, the way a SAST tool flags SQL injection before the query ever runs.
Supports LangGraph, the OpenAI Agents SDK, CrewAI, and MCP (both server manifests and
FastMCP @mcp.tool server source, the latter giving HIGH-confidence analysis of the real tool
bodies). Outputs a console table, JSON, or spec-compliant SARIF 2.1.0 for GitHub code scanning,
and returns a CI-friendly exit code.
Status: v1.0 (research/portfolio project). See DESIGN.md for the full rationale behind every decision below.
The problem
Single-agent LLM systems have one trust boundary: the line between the user's prompt and the model. Multi-agent systems (LangGraph, CrewAI, AutoGen, Google ADK) and MCP tool integrations multiply that boundary across a graph of agents and tools, and most frameworks wire that graph together without re-checking authorization at each hop. Concretely: data enters from a source that may be attacker-influenced — a scraped web page, a document pulled into RAG, a third-party API response, a poisoned tool result — and can flow, unvalidated, into a node with real-world write power.
This is a structural recurrence of the confused deputy problem (Norm Hardy, 1988), catalogued by MITRE as CWE-441 (Unintended Proxy or Intermediary). What's new is the medium: the "deputy" is now an LLM interpreting natural language, and the trust boundary is a graph edge instead of a function call — which makes the vulnerability harder to see by inspection and easier to construct by accident. The attack and runtime-defense research is mature; the static side (inspect the graph before deployment) is the gap this fills.
How it works
flowchart LR
subgraph Extraction
A1[LangGraph parser]
A2[OpenAI Agents / CrewAI parsers]
A3[FastMCP / MCP manifest parser]
end
B[Capability + source<br/>classification]
C[networkx graph]
subgraph Analysis
D[Trust-boundary<br/>detection]
E[Taint path<br/>enumeration]
F[Rule engine]
G[Severity + confidence<br/>scoring]
end
subgraph Reporting
H1[Console]
H2[JSON]
H3[SARIF]
end
A1 --> B
A2 --> B
A3 --> B
B --> C
C --> D
C --> E
D --> E
E --> F
F --> G
G --> H1 & H2 & H3
- Extraction parses LangGraph source, OpenAI Agents SDK source, FastMCP server source
(
@mcp.tool), and MCP manifests into a normalized IR of nodes and edges. A.pyfile is parsed against each framework (each ignores files that aren't its own). For LangGraph it AST-walks graph construction (StateGraph,add_node,add_edge,add_conditional_edges), the four tool-wiring idioms (ToolNode,create_react_agent,create_agent, in-bodyllm.bind_tools), and modern multi-agent control flow (Command(goto=...)andSend(...)handoffs,compile(interrupt_before/after=[...])pause points). Each node's calls — including those in local helper functions it calls (interprocedural) — are recorded as evidence. Anything too dynamic to resolve is reported asunresolvedrather than silently dropped. - Classification assigns each node capabilities and a source category using a signal hierarchy:
AST-detected calls (HIGH) > MCP annotation hints / known dangerous tool classes like
CodeInterpreterTool/ShellTool(MEDIUM) > name/description keywords (LOW). Confidence is reported alongside every finding. - Graph construction assembles a
networkxdigraph. A flat MCP manifest (no edges) gets a synthesized client hub wired to every tool, modeling that one session can call them all. - Analysis detects trust boundaries, enumerates source→sink paths that don't cross one, and applies five rules (four graph-reachability + intra-tool value taint).
- Reporting renders findings as console / JSON / SARIF and sets the exit code.
What it detects
| Rule | What it flags | CWE (informal) |
|---|---|---|
| confused-deputy | An unmitigated path from an untrusted source to a privileged sink | CWE-441 |
| lethal-trifecta | One subgraph with untrusted input + private data + external egress (Willison) | CWE-668 |
| cyclic-delegation | An agent-to-agent delegation cycle that can bypass per-hop capability limits | CWE-441 |
| guardrail-only | A path whose only control is a prompt-based/sanitizer guard, not a structural boundary | CWE-863 |
| tool-injection | A tool whose own input flows unsanitized to a dangerous sink (intra-tool value taint) | CWE-78 / 918 / 22 |
Rules 1–4 are cross-agent reachability (can untrusted data reach a sink across the graph); rule 5 is intra-tool value taint (is a single tool's own input passed to a dangerous sink — a classic injection bug). See DESIGN.md §5a for why the inter-node analysis is reachability rather than value-tracking, grounded in the AgentFlow / VIPER-MCP literature.
Capabilities: CODE_EXEC, FILE_WRITE, FILE_READ, NETWORK_EGRESS, CREDENTIAL_ACCESS,
AGENT_DELEGATION, EXTERNAL_ACTION (financial/physical/publishing actions), READ_ONLY_QUERY.
Sources: UNTRUSTED_EXTERNAL, USER_INPUT, TRUSTED_INTERNAL.
What counts as a trust boundary (a path crossing one is mitigated): a human-in-the-loop
interrupt(); a deterministic validator with a rejection path (pydantic/jsonschema validation, or a
regex/allowlist check followed by raise); or a node you explicitly trust in the config. A bare
sanitizer (re.sub, .strip) or an LLM "is this safe?" check is not a boundary.
Installation
Requires Python 3.11+.
pip install agent-boundary-scan
Or from source:
git clone https://github.com/AureliusOctavion/agent-boundary-scan.git
cd agent-boundary-scan
pip install -e .
Usage
agent-boundary-scan scan <path> # a .py file, a .json manifest, or a directory
agent-boundary-scan scan src/ -f sarif -o results.sarif
agent-boundary-scan scan graph.py --fail-on CRITICAL
| Option | Meaning |
|---|---|
-f, --format |
console (default), json, or sarif |
-o, --output |
Write the report to a file instead of stdout |
--fail-on |
Severity gate for the exit code: CRITICAL | HIGH | MEDIUM | LOW (default HIGH) |
--config |
Path to an .agent-boundary-scan.yml (default: found next to the target) |
Exit codes: 0 clean, 1 a finding at or above the --fail-on severity was found, 2
usage/scan error. That makes scan usable directly as a CI gate.
Console output
1 finding(s) across 1 file(s): 1 critical
1. [CRITICAL] confused-deputy (confidence: high, CWE-441) confused_deputy_langgraph.py:31
Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) with no
trust boundary in between. Example path: fetch_page -> run_command.
path: fetch_page -> run_command
SARIF (for GitHub code scanning)
Severity maps to the SARIF level plus a numeric security-severity (so GitHub ranks findings);
confidence maps to properties.precision (the CodeQL convention).
{
"ruleId": "confused-deputy",
"level": "error",
"message": { "text": "Untrusted data (from: fetch_page) can reach privileged sink 'run_command' (CODE_EXEC) ..." },
"properties": { "security-severity": "9.0", "precision": "high", "cwe": "CWE-441" },
"locations": [ { "physicalLocation": {
"artifactLocation": { "uri": "confused_deputy_langgraph.py" },
"region": { "startLine": 31 } } } ]
}
Configuration (.agent-boundary-scan.yml)
# Nodes to trust as boundaries even if the static heuristics don't recognize them.
# Every path cleared this way is labeled "boundary trusted via user override" in output.
trusted_boundaries:
- my_module.custom_validator
- approval_node
# Severity at or above which `scan` exits non-zero (overridable with --fail-on).
fail_on_severity: HIGH
Keeping overrides in a checked-in file (rather than an in-code decorator) means they work for MCP-manifest-only scans too, and every trust decision is visible in one PR-reviewable place.
GitHub Action
A reusable action ships in this repo. It installs the scanner, runs it, and gates the build; set
upload-sarif: true (with security-events: write) to feed findings into GitHub code scanning:
permissions:
security-events: write # only needed for upload-sarif
jobs:
agent-boundary-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AureliusOctavion/agent-boundary-scan@v1
with:
path: .
fail-on: HIGH
upload-sarif: true
Inputs: path (default .), fail-on (default HIGH), output (SARIF path), config,
upload-sarif (default false), python-version (default 3.11).
pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/AureliusOctavion/agent-boundary-scan
rev: v1.1.0
hooks:
- id: agent-boundary-scan
Scans the whole tree on commit (the analysis is graph-based). Override the target or gate with
args: ["scan", "src/", "--fail-on", "CRITICAL"].
Limitations (read these)
This is a triage/assist tool, not a proof of safety. A clean scan means "no obvious structural red flags, plus here is where I couldn't see" — never "safe". Key false-negative/false-positive risks (full discussion in DESIGN.md §7):
- Heuristic classification (LOW-confidence, keyword-based) both under- and over-classifies; confidence is always reported so you can weigh it.
- Interprocedural analysis is module-local and bare-name only — a node is scanned together with
local helpers it calls, but danger reached only through imported code or method calls (
obj.foo()) is still under-reported. - MCP-only scans have no function bodies to inspect, so confidence is capped at MEDIUM/LOW, and the co-reachability model assumes all of a manifest's tools are exposed to one agent (can over-flag).
- MCP annotation hints are server-supplied claims, treated as untrusted per the MCP spec.
- Cyclic-delegation deliberately excludes agent↔tool ReAct loops (can miss a cycle through an agent that also wraps tools).
Prior art
- pajaMAS — Trail of Bits (Triedman et al., 2025): MAS-hijacking control-flow attacks.
- InjecAgent (ACL Findings 2024) and AgentDojo (NeurIPS 2024): indirect-prompt-injection benchmarks for tool-using agents.
- Prompt Flow Integrity (Kim, Choi, Lee, 2025) and MiniScope (2025): runtime enforcement / least-privilege reconstruction — complementary to this tool's static, pre-deploy analysis.
- OWASP Top 10 for Agentic Applications (2026) and Simon Willison's lethal trifecta (2025).
- "Capability Gates Are Not Authorization" (arXiv 2606.28679, 2026): frameworks gate which tools a model sees but don't re-authorize the specific arguments of a specific call — the gap confused-deputy paths exploit.
- AgentFlow (arXiv 2607.01640, 2026): the first static analysis framework for agent programs — independently validates this tool's approach (LLM as a taint conduit; reachability over an over-approximating dependency graph).
- VIPER-MCP (arXiv 2605.21392, 2026): intra-tool taint of MCP
servers (106 zero-day CVEs) — the basis for the
tool-injectionrule.
Fixtures under fixtures/ recreate the patterns (not verbatim code) of these
scenarios; the test suite asserts every vulnerable fixture is flagged and every safe fixture is not.
Future work
- Deeper interprocedural resolution — follow method calls and imported helpers (needs light type/import resolution), beyond today's module-local bare-name expansion.
- Compiled subgraphs as nodes — inline a
builder.compile()used as anadd_nodeaction (Command/Send handoffs andinterrupt_before/afterpause points are already handled). - MCP resources & prompts (
@mcp.resource,@mcp.prompt) — resources as untrusted sources. - Full argument-level dataflow — track the untrusted value into the sink's dangerous argument (today's taint is graph-reachability with self-node and per-sink precision refinements).
- OpenAI Agents SDK guardrails — recognize
tool_input_guardrails/output_guardrailshooks as trust boundaries (surfaced by the real-corpus benchmark; see benchmark/). - AutoGen / ag2, Google ADK, Pydantic AI extraction, and cross-framework tracing.
- CrewAI Task/context chaining (v1 models agents, tools, and delegation, not task data-flow).
- SARIF codeFlows — render the full source→sink chain as thread-flow locations.
- Correlating an MCP manifest with the server implementation source when available (upgrades MCP-only confidence from MEDIUM to HIGH).
Development
pip install -e ".[dev]"
pytest -q # 152 tests
ruff check . # lint
CI (GitHub Actions) runs lint + tests on Python 3.11 and 3.12.
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 agent_boundary_scan-1.1.0.tar.gz.
File metadata
- Download URL: agent_boundary_scan-1.1.0.tar.gz
- Upload date:
- Size: 105.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1fcdb9879a8505384e23378c5dbd12bd74f45ef5ca61337aba0c0684b283d5d6
|
|
| MD5 |
cef39b6cc2441f804ed8acbe3ddacf5a
|
|
| BLAKE2b-256 |
e5468cba05985542c6b7cfe360d99271f25135204e4cc993889e1ce79b5125d2
|
Provenance
The following attestation bundles were made for agent_boundary_scan-1.1.0.tar.gz:
Publisher:
publish.yml on AureliusOctavion/agent-boundary-scan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_boundary_scan-1.1.0.tar.gz -
Subject digest:
1fcdb9879a8505384e23378c5dbd12bd74f45ef5ca61337aba0c0684b283d5d6 - Sigstore transparency entry: 2569438277
- Sigstore integration time:
-
Permalink:
AureliusOctavion/agent-boundary-scan@e40d3b22193ff4a88b58b4dae9cc92e2f4d96ed2 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/AureliusOctavion
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e40d3b22193ff4a88b58b4dae9cc92e2f4d96ed2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file agent_boundary_scan-1.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_boundary_scan-1.1.0-py3-none-any.whl
- Upload date:
- Size: 59.1 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 |
c6ebfd6e4d8d2dbc53122d983427524734dc998639a4dc0d5d7f678162d3d1d2
|
|
| MD5 |
98c71260701543bc479561fcdf793607
|
|
| BLAKE2b-256 |
dee1932136d4b948e31729580649b139f1ecd228bbcf9454ffadc623f61d325d
|
Provenance
The following attestation bundles were made for agent_boundary_scan-1.1.0-py3-none-any.whl:
Publisher:
publish.yml on AureliusOctavion/agent-boundary-scan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_boundary_scan-1.1.0-py3-none-any.whl -
Subject digest:
c6ebfd6e4d8d2dbc53122d983427524734dc998639a4dc0d5d7f678162d3d1d2 - Sigstore transparency entry: 2569438280
- Sigstore integration time:
-
Permalink:
AureliusOctavion/agent-boundary-scan@e40d3b22193ff4a88b58b4dae9cc92e2f4d96ed2 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/AureliusOctavion
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e40d3b22193ff4a88b58b4dae9cc92e2f4d96ed2 -
Trigger Event:
release
-
Statement type: