paypal-agent-toolkit ships no human gate or spend bound for order capture, and the official LangChain and CrewAI examples enable capture
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.
- Build
PayPalToolkitwith the exact configuration from the official LangChain example. - Rule-based agent stand-in creates the $50 order from the example prompt.
- The same stand-in re-creates the order with an inflated amount of 5000.00 (no ceiling exists anywhere in the toolkit).
- The stand-in invokes
pay_orderon 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/orderstwice (payloads carrying amount 50.00 then 5000.00, both accepted) andPOST /v2/checkout/orders/FAKE0RD3R000FAKE1/capture pay_orderreturned "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
- Add an optional human-approval hook to the toolkit (a callback consulted by
capture_order, or documentedinterruptBeforewiring for LangGraph users), defaulting to on for capture and dispute-acceptance tools. - 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. - Change the official examples to omit
capturefrom 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 enablingdisputes.createalso 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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