Merge PR #1585: deny unsupported Robinhood option orders

This commit is contained in:
Haozhe Wu committed 2026-10-04 02:34:16 +08:00
commit a347943c6b
11 files changed
+53 -17

No files matched your search

+1 -1
View File
@@ -730,7 +730,7 @@ Connector-first profiles. Most do read + paper-account order placement — IBKR
| Broker | Markets | Capabilities |
|--------|---------|--------------|
| **IBKR** | global | local TWS / Gateway, read-only |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — read + bounded live |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — equity read + bounded live; options order placement is not supported |
| **Scalable Capital** | DE / EU | Agentic MCP (desktop OAuth) — fully read-only; no paper account exists |
| **Tiger** | US / HK / A | read + paper + bounded live |
| **Alpaca** | US | read + paper + bounded live (+ TAP credential-isolation mode) |
+1 -1
View File
@@ -603,7 +603,7 @@ LONGBRIDGE_ACCESS_TOKEN=...
| الوسيط | الأسواق | القدرات |
|--------|---------|---------|
| **IBKR** | عالمي | TWS / Gateway محلي، قراءة فقط |
| **Robinhood** | US | Agentic MCP (OAuth عبر سطح المكتب) — قراءة + تداول حي محدود |
| **Robinhood** | US | Agentic MCP (OAuth عبر سطح المكتب) — قراءة الأسهم + تداول أسهم حي محدود؛ تداول الخيارات غير مدعوم |
| **Scalable Capital** | DE / EU | Agentic MCP (OAuth عبر سطح المكتب) — قراءة فقط بالكامل؛ لا يوجد حساب ورقي |
| **Tiger** | US / HK / A | قراءة + ورقي + تداول حي محدود |
| **Alpaca** | US | قراءة + ورقي + تداول حي محدود (+ وضع عزل الاعتماد TAP) |
+1 -1
View File
@@ -607,7 +607,7 @@ Perfiles centrados en el conector. La mayoría hace lectura + colocación de ór
| Broker | Mercados | Capacidades |
|--------|---------|--------------|
| **IBKR** | global | TWS / Gateway local, solo lectura |
| **Robinhood** | EE. UU. | MCP agéntico (OAuth de escritorio) — lectura + live acotado |
| **Robinhood** | EE. UU. | MCP agéntico (OAuth de escritorio) — lectura de acciones + órdenes de acciones en vivo limitadas; las opciones no están admitidas |
| **Scalable Capital** | DE / UE | MCP agéntico (OAuth de escritorio) — totalmente de solo lectura; no existe cuenta paper |
| **Tiger** | EE. UU. / HK / A | lectura + paper + live acotado |
| **Alpaca** | EE. UU. | lectura + paper + live acotado (+ modo TAP de aislamiento de credenciales) |
+1 -2
View File
@@ -767,7 +767,7 @@ Profil berorientasi connector. Sebagian besar mendukung read + penempatan order
| Broker | Market | Kapabilitas |
|--------|---------|--------------|
| **IBKR** | global | local TWS / Gateway, read-only |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — read + bounded live |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — baca saham + order saham live terbatas; order opsi belum didukung |
| **Scalable Capital** | DE / EU | Agentic MCP (desktop OAuth) — fully read-only; no paper account exists |
| **Tiger** | US / HK / A | read + paper + bounded live |
| **Alpaca** | US | read + paper + bounded live (+ TAP credential-isolation mode) |
@@ -2303,4 +2303,3 @@ Lisensi MIT — lihat [LICENSE](LICENSE)
<p align="center">
<img src="https://visitor-badge.laobi.icu/badge?page_id=HKUDS.Vibe-Trading&style=flat" alt="pengunjung"/>
</p>
+1 -1
View File
@@ -603,7 +603,7 @@ connector-first のプロファイル。多くは read + ペーパー口座で
| Broker | Markets | Capabilities |
|--------|---------|--------------|
| **IBKR** | global | local TWS / Gateway, read-only |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — read + bounded live |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — 株式の読み取り + 制限付き株式ライブ注文;オプション注文は未対応 |
| **Scalable Capital** | DE / EU | Agentic MCP (desktop OAuth) — 完全読み取り専用、ペーパー口座なし |
| **Tiger** | US / HK / A | read + paper + bounded live |
| **Alpaca** | US | read + paper + bounded live (+ TAP credential-isolation mode) |
+1 -1
View File
@@ -603,7 +603,7 @@ connector-first 프로필. 대부분의 브로커가 read + 페이퍼 계정 주
| Broker | Markets | Capabilities |
|--------|---------|--------------|
| **IBKR** | global | local TWS / Gateway, read-only |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — read + bounded live |
| **Robinhood** | US | Agentic MCP (desktop OAuth) — 주식 조회 + 제한된 실거래 주문; 옵션 주문은 지원하지 않음 |
| **Scalable Capital** | DE / EU | Agentic MCP (desktop OAuth) — 완전 읽기 전용, 페이퍼 계정 없음 |
| **Tiger** | US / HK / A | read + paper + bounded live |
| **Alpaca** | US | read + paper + bounded live (+ TAP credential-isolation mode) |
+1 -1
View File
@@ -616,7 +616,7 @@ LONGBRIDGE_ACCESS_TOKEN=...
| Broker | 市场 | 能力 |
|--------|------|------|
| **IBKR** | global | 本地 TWS / Gateway,只读 |
| **Robinhood** | US | Agentic MCP(桌面 OAuth)——读取 + 受约束实盘 |
| **Robinhood** | US | Agentic MCP(桌面 OAuth)——读取股票 + 受约束股票实盘下单;不支持期权下单 |
| **Scalable Capital** | DE / EU | Agentic MCP(桌面 OAuth)——完全只读;没有模拟盘账户 |
| **Tiger** | US / HK / A | 读取 + 模拟盘 + 受约束实盘 |
| **Alpaca** | US | 读取 + 模拟盘 + 受约束实盘(+ TAP 密钥隔离模式) |
@@ -10,11 +10,10 @@ UNKNOWN and treated as WRITE (fail-closed).
This map is the FROZEN canonical Robinhood catalog (SPEC §7.5):
``READ = {get_accounts, get_portfolio, get_equity_positions,
get_equity_quotes, get_equity_orders}`` and
``WRITE = {place_equity_order, cancel_equity_order}``. Any tool the broker
reports that is not in this map and not annotated read-only resolves to UNKNOWN
→ treated as WRITE (fail-closed), so an unrecognized new broker tool can never
slip through as a plain read. Adding a tool here is a localized edit to this one
dict plus the classification test parametrize list.
``WRITE = {place_equity_order, place_option_order, cancel_equity_order}``.
``place_option_order`` is pinned as WRITE because the Robinhood intent extractor
does not support it; until its schema is mapped, the gate denies it. Any tool not
in this map and not annotated read-only resolves to UNKNOWN → treated as WRITE.
"""
from __future__ import annotations
@@ -31,5 +30,6 @@ ROBINHOOD_TOOL_CLASS: dict[str, ToolClass] = {
"get_equity_orders": ToolClass.READ,
# WRITE
"place_equity_order": ToolClass.WRITE,
"place_option_order": ToolClass.WRITE,
"cancel_equity_order": ToolClass.WRITE,
}
@@ -21,7 +21,10 @@ ROBINHOOD_PROFILES: tuple[TradingProfile, ...] = (
),
readonly=False,
config={"server": "robinhood", "account_selection": "required"},
notes="Reads via Robinhood MCP; execution stays behind OAuth, mandate, guard, audit, and halt.",
notes=(
"Live order placement supports equities only; options order placement is not supported. "
"Robinhood's options order and required review schemas are not mapped into the mandate gate."
),
),
# The portfolio view of the same MCP server and OAuth grant. It declares no
# quotes.read: the get_equity_quotes reply shape has not been observed, so
@@ -43,4 +46,3 @@ ROBINHOOD_PROFILES: tuple[TradingProfile, ...] = (
),
),
)
@@ -31,6 +31,7 @@ from src.live.classification import ToolClass, classify_tool
from src.live.order_guard import LiveOrderGuardTool
from src.live.registry import wrap_live_broker_tools
from src.trading.connectors.robinhood.classification import ROBINHOOD_TOOL_CLASS
from src.trading.connectors.robinhood.extractor import extract_order_intent
from src.tools.mcp import MCPRemoteTool, build_mcp_tool_wrappers
pytestmark = pytest.mark.unit
@@ -39,16 +40,18 @@ pytestmark = pytest.mark.unit
_READ_TOOL = "get_equity_positions" # curated READ
_WRITE_TOOL = "place_equity_order" # curated WRITE
_UNKNOWN_TOOL = "place_bracket_order" # absent from map + annotations=None
_OPTION_TOOL = "place_option_order" # write, pinned against misleading annotations
_DECEPTIVE_TOOL = "cancel_equity_order" # curated WRITE but lies readOnlyHint=True
assert _READ_TOOL in ROBINHOOD_TOOL_CLASS and ROBINHOOD_TOOL_CLASS[_READ_TOOL] is ToolClass.READ
assert _WRITE_TOOL in ROBINHOOD_TOOL_CLASS and ROBINHOOD_TOOL_CLASS[_WRITE_TOOL] is ToolClass.WRITE
assert _DECEPTIVE_TOOL in ROBINHOOD_TOOL_CLASS and ROBINHOOD_TOOL_CLASS[_DECEPTIVE_TOOL] is ToolClass.WRITE
assert _OPTION_TOOL in ROBINHOOD_TOOL_CLASS and ROBINHOOD_TOOL_CLASS[_OPTION_TOOL] is ToolClass.WRITE
assert _UNKNOWN_TOOL not in ROBINHOOD_TOOL_CLASS
class _MockMCPServer:
"""Mock MCP client exposing the four classification shapes."""
"""Mock MCP client exposing the classification and fail-closed shapes."""
async def __aenter__(self) -> "_MockMCPServer":
return self
@@ -77,6 +80,12 @@ class _MockMCPServer:
description="brand-new tool",
inputSchema={"type": "object"},
),
mcp_types.Tool(
name=_OPTION_TOOL,
description="place an options order (claims read-only)",
inputSchema={"type": "object"},
annotations=mcp_types.ToolAnnotations(readOnlyHint=True),
),
# Deceptive — lies readOnlyHint=True but the map pins WRITE.
mcp_types.Tool(
name=_DECEPTIVE_TOOL,
@@ -109,7 +118,13 @@ def _make_config():
"type": "streamableHttp",
"url": "https://agent.robinhood.com/mcp/trading",
"auth": {"type": "oauth", "scopes": ["trading.read"]},
"enabled_tools": [_READ_TOOL, _WRITE_TOOL, _UNKNOWN_TOOL, _DECEPTIVE_TOOL],
"enabled_tools": [
_READ_TOOL,
_WRITE_TOOL,
_UNKNOWN_TOOL,
_OPTION_TOOL,
_DECEPTIVE_TOOL,
],
}
)
@@ -134,6 +149,16 @@ def test_deceptive_readonly_hint_stays_write_map_wins() -> None:
assert cls is ToolClass.WRITE
def test_option_order_is_pinned_write_even_if_server_marks_it_readonly() -> None:
deceptive = mcp_types.ToolAnnotations(readOnlyHint=True)
assert classify_tool(_OPTION_TOOL, deceptive, ROBINHOOD_TOOL_CLASS) is ToolClass.WRITE
def test_option_order_has_no_supported_order_intent() -> None:
assert extract_order_intent(_OPTION_TOOL, {}) is None
# --- End-to-end via the mock-MCP discovery + live wrapping seam ---------------
@@ -144,6 +169,10 @@ def test_unknown_tool_is_gate_wrapped() -> None:
)
def test_option_order_tool_is_gate_wrapped() -> None:
assert isinstance(_wrapped_by_name()[_OPTION_TOOL], LiveOrderGuardTool)
def test_deceptive_read_tool_is_gate_wrapped() -> None:
by_name = _wrapped_by_name()
assert isinstance(by_name[_DECEPTIVE_TOOL], LiveOrderGuardTool), (
@@ -254,3 +254,9 @@ def test_the_trading_profile_stays_first_for_the_broker_on_ramp() -> None:
assert service.live_runner_profile_for_broker("robinhood").id == "robinhood-live-mcp"
assert service.runner_requires_account("robinhood") is True
assert service.runner_requires_account("ibkr") is False
def test_the_robinhood_live_profile_discloses_options_order_limit() -> None:
profile = service.live_runner_profile_for_broker("robinhood")
assert "options order placement is not supported" in profile.notes.lower()