paypal / paypal/agent-toolkit

paypal-agent-toolkit ships no human gate or spend bound for order capture, and the official LangChain and CrewAI examples enable capture

Open
#95 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
194
Forks
114
Avg merge
3d 13h
Merged PRs (30d)
1

Description

Affected repo: paypal/agent-toolkit, python/ (shipped as paypal-agent-toolkit 1.10.0)
Pinned commit: 2fe2525866247d6875977282048bb6a3074b0805 (main, 2026-08-20)
PUBLIC-READY
AW-02 (uncapped autopay example), instance R06P2-F3
Honesty ledger: EXECUTED (PoC at pin, offline stub PayPalClient), MEASURED (zero approval-mechanism identifiers across the package), DERIVED (payer-side approval note)

Summary

The Python PayPal toolkit hands LangChain and CrewAI agents direct create_order and pay_order (capture) tools. There is no approval hook, interruption point, amount ceiling, or payee validation anywhere in the package: a grep for mechanism identifiers (approval_callback, human_approval, require_approval, ask_human, wait_for_approval, interrupt_before, interrupt_fn, confirmation_required) across all 38 Python modules returns zero matches at the pin.

The Configuration(actions=...) filter decides only which tools exist; it does not bound any call. Both official framework examples (python/examples/langchain/app_agent.py and python/examples/crewai/app_agent.py) enable orders: {create: True, get: True, capture: True} and prompt the agent with a dollar amount ("Create an PayPal order for $50 for Premium News service."), making the LLM the sole decider of amount and of when capture fires.

What bounds the call today is only PayPal's own payer-side approval for payer-initiated checkout flows. Everything on the toolkit side, the side this package controls, is unbounded.

Steps to reproduce (offline, dummy credentials)

PoC: poc/f3_paypal_no_gate.py in the R06P2 workspace. It stubs PayPalClient.post to record calls and return canned PayPal-shaped responses; no network and dummy sandbox credentials only.

  1. Build PayPalToolkit with the exact configuration from the official LangChain example.
  2. Rule-based agent stand-in creates the $50 order from the example prompt.
  3. The same stand-in re-creates the order with an inflated amount of 5000.00 (no ceiling exists anywhere in the toolkit).
  4. The stand-in invokes pay_order on the order.

Observed (deterministic across two runs, poc/out/f3_run1.txt equals f3_run2.txt):

  • tools handed to the agent: create_order, get_order_details, pay_order
  • HTTP calls recorded: POST /v2/checkout/orders twice (payloads carrying amount 50.00 then 5000.00, both accepted) and POST /v2/checkout/orders/FAKE0RD3R000FAKE1/capture
  • pay_order returned "The PayPal order ... has been successfully captured" with no gate, callback, or confirmation step in between
  • approval mechanism identifiers present in package: 0

Impact

Any deployment that follows the official examples gives the model an unbounded money-movement primitive. A prompt injection, a hallucinated price, or a routed-tool misuse converts directly into a capture attempt with an attacker-influenced amount. The actions configuration gives operators the illusion of a policy dial, but it is tool-presence filtering, not call bounding.

Suggested fix

  1. Add an optional human-approval hook to the toolkit (a callback consulted by capture_order, or documented interruptBefore wiring for LangGraph users), defaulting to on for capture and dispute-acceptance tools.
  2. Support per-tool spend bounds in Configuration (for example a max capture amount and a payee allowlist for invoices) enforced inside the tool handlers before any HTTP call.
  3. Change the official examples to omit capture from the default action set, or to route capture through an approval step, so the copy-paste path is not the unbounded one.

Notes

  • Side observation: accept_dispute_claim (conceding a dispute, a money-losing action) is gated under {"disputes": {"create": True}}, so an operator enabling disputes.create also enables claim concession under a misleading key.
  • The OR-style is_tool_allowed (a tool is exposed if any of its declared actions is enabled) currently reduces to exact matching because each tool declares exactly one action, but it will silently over-expose if a tool ever declares multiple actions.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with python/examples/langchain/app_agent.py and python/examples/crewai/app_agent.py, then inspect Configuration and the capture tool handlers in the Python package. Run poc/f3_paypal_no_gate.py to reproduce the unbounded calls. Done means capture has an approval path and enforced spend or payee bounds, and the official examples no longer expose an unbounded capture path.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
payments, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.