Skip to main content

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
  1. 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 .py file 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-body llm.bind_tools), and modern multi-agent control flow (Command(goto=...) and Send(...) 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 as unresolved rather than silently dropped.
  2. 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.
  3. Graph construction assembles a networkx digraph. A flat MCP manifest (no edges) gets a synthesized client hub wired to every tool, modeling that one session can call them all.
  4. Analysis detects trust boundaries, enumerates source→sink paths that don't cross one, and applies five rules (four graph-reachability + intra-tool value taint).
  5. 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-injection rule.

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 an add_node action (Command/Send handoffs and interrupt_before/after pause 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_guardrails hooks 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

agent_boundary_scan-1.1.0.tar.gz (105.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_boundary_scan-1.1.0-py3-none-any.whl (59.1 kB view details)

Uploaded Python 3

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

Hashes for agent_boundary_scan-1.1.0.tar.gz
Algorithm Hash digest
SHA256 1fcdb9879a8505384e23378c5dbd12bd74f45ef5ca61337aba0c0684b283d5d6
MD5 cef39b6cc2441f804ed8acbe3ddacf5a
BLAKE2b-256 e5468cba05985542c6b7cfe360d99271f25135204e4cc993889e1ce79b5125d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_boundary_scan-1.1.0.tar.gz:

Publisher: publish.yml on AureliusOctavion/agent-boundary-scan

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_boundary_scan-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_boundary_scan-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c6ebfd6e4d8d2dbc53122d983427524734dc998639a4dc0d5d7f678162d3d1d2
MD5 98c71260701543bc479561fcdf793607
BLAKE2b-256 dee1932136d4b948e31729580649b139f1ecd228bbcf9454ffadc623f61d325d

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page