Documentation
This directory contains developer-facing usage, production, and operations documentation.
Start Here
- Developer Guide: install, CLI, SDK, MCP, order workflows, audit, and validation commands.
- Production Readiness: deployment checklist, secrets, remote MCP, audit, sidecar, paper, and live gates.
- Public API: supported Rust facade modules and examples.
- Testing: local/CI gates, fixture coverage, replay, secret regression, and performance budgets.
Feature Guides
- Client Portal Gateway: local broker session expectations and troubleshooting.
- Read and Tool Commands: read-only data, MCP maturity tools, preview, paper, live-gated, bracket, and sidecar commands.
- Local and Remote MCP: MCP transports, tool registry, forbidden generic write tools, and provider usage.
- Remote MCP OAuth/OIDC: protected resource metadata, RS256/JWKS validation, token-id hashing, rate limiting, and connection caps.
- Order Preview: non-executable previews and deterministic risk checks.
- Paper Orders: approval and idempotent paper lifecycle.
- Paper to Live and Live Runbook: live trading gates and operational checklist.
- Sidecar Relay: pairing, heartbeat, and local CP Gateway forwarding boundary.
- Provider Compatibility: MCP client targets and provider-neutral compatibility checks.
- Audit Log, Audit Retention, and Incident Review: evidence handling and redacted review workflows.
Developer Guide
This guide is the shortest path for developers who want to use or extend
ibkr-agent-gateway.
Install and Run
From this checkout:
cargo run --bin ibkr-agent -- health --json
cargo run --bin ibkr-agent -- accounts list --json
Install the CLI locally:
cargo install --path .
ibkr-agent health --json
Embed the library from another local Rust project:
cargo add ibkr-agent-gateway --path /path/to/ibkr-agent-gateway
use ibkr_agent_gateway::prelude::*;
#[tokio::main]
async fn main() -> Result<(), GatewayError> {
let gateway = Gateway::new(GatewayConfig::fake_local())?;
let accounts = gateway.list_accounts().await?;
println!("visible_accounts={}", accounts.len());
Ok(())
}
Backend Modes
Use GatewayConfig::fake_local() while developing. It reads deterministic
fixtures from tests/fixtures/cpapi/ and needs no broker session.
Use GatewayConfig::client_portal(url) only when the Interactive Brokers
Client Portal Gateway is already running and manually authenticated outside this
project. TLS verification can be disabled only for localhost URLs.
For production-like deployments, read
production-readiness.md. The CLI runner defaults to
fake fixtures only when --config is omitted. A real broker CLI run should pass
an explicit YAML config, such as config/local.example.yaml, and supply the
configured audit HMAC secret through the environment.
CLI Surface
Read and session inspection:
ibkr-agent backend status --json
ibkr-agent session requirements --json
ibkr-agent account summary --account DU1234567 --json
ibkr-agent portfolio snapshot --account DU1234567 --json
ibkr-agent positions list --account DU1234567 --json
ibkr-agent market snapshot --contract-id 265598 --json
ibkr-agent executions list --account DU1234567 --json
Order preview is non-executable and must be explicitly enabled:
ibkr-agent orders preview \
--account DU1234567 \
--symbol AAPL \
--side buy \
--quantity 1 \
--limit-price 100 \
--currency USD \
--enable-preview \
--json
Paper submit/cancel commands require explicit paper enablement and an
idempotency key. Submit also requires the approval id returned by
approvals create:
ibkr-agent approvals create --account DU1234567 --preview-id <preview_id> --ttl-seconds 300 --json
ibkr-agent orders submit --account DU1234567 --approval-id <approval_id> --idempotency-key paper-submit-001 --enable-paper --json
ibkr-agent orders cancel --account DU1234567 --broker-order-id paper-order-local --idempotency-key paper-cancel-001 --enable-paper --json
CLI live submit and cancel run the full gate stack and then call a
LiveOrderWriter. The CLI defaults to LocalCandidateLiveWriter, so the
broker order id is a deterministic local-candidate-* value. Use
--live-broker client-portal with a Client Portal Gateway config to call
ClientPortalLiveWriter from the CLI; --live-broker refusing is available
for fail-closed checks. Production deployments can also inject their own
writer through the SDK boundary (see docs/production-readiness.md):
ibkr-agent orders live-submit \
--account DU1234567 \
--approval-id <approval_id> \
--idempotency-key live-submit-001 \
--enable-live \
--live-scope \
--open-kill-switch \
--acknowledge-paper-to-live \
--live-broker local-candidate \
--json
Audit review:
ibkr-agent audit tail --limit 20 --json
ibkr-agent audit export --limit 500 --json
ibkr-agent audit verify --json
MCP
Local stdio MCP:
ibkr-agent mcp serve --transport stdio --describe --json
ibkr-agent mcp serve --transport stdio --json
--describe exits after a smoke-check description. Without it, the command
runs the stdio JSON-RPC loop, advertises only tools enabled by local scopes, and
audits every tool call.
Remote HTTP MCP is disabled by default. Enabling it requires complete
remote MCP runtime config, an OAuth/OIDC issuer, RS256/RSA JWKS validation, a
token-id HMAC secret, accepted audiences, allowed scopes, and the independent
safety flag. In CLI YAML, that safety flag is safety.remote_mcp_enabled; the
SDK-facing GatewayConfiguration field is safety.remote_public_mcp_enabled.
ibkr-agent --config config/remote.example.yaml mcp serve --transport http --enable-remote-mcp --bind 127.0.0.1:8080
The HTTP listener serves protected-resource metadata and routes authorized
JSON-RPC requests on POST /mcp. See
remote-mcp-oauth.md.
Example client configs live under examples/mcp-clients/.
Validation
Run these before changing package behavior:
cargo fmt --check
cargo clippy --workspace --all-targets --features unstable-internal-test-support -- -D warnings
cargo test --workspace --features unstable-internal-test-support
cargo test --workspace --features unstable-internal-test-support secret
Run packaging checks before release work:
cargo package --allow-dirty --no-verify --list
cargo publish --dry-run --locked
Safety Rules for Contributors
- Do not return broker cookies, bearer tokens, credentials, raw headers, local paths, or raw session material from any CLI, MCP, log, fixture, or audit path.
- Keep broker logic provider-neutral; provider-specific behavior belongs in compatibility examples and tests.
- Keep write-capable flows fail-closed unless explicit config, scope, approval, idempotency, audit, and risk gates are satisfied.
- Preserve the public facade boundary in
src/public/*andsrc/lib.rs; keep implementation details undersrc/internal/*.
Getting Started Locally
This page is kept for the original MVP path. For the complete current package guide, start with developer-guide.md.
Local Fake Backend
The fastest development path uses fake Client Portal Gateway fixtures:
cargo run --bin ibkr-agent -- health --json
cargo run --bin ibkr-agent -- backend status --json
cargo run --bin ibkr-agent -- session requirements --json
cargo run --bin ibkr-agent -- accounts list --json
When no --config path is provided, the CLI uses fixtures under
tests/fixtures/cpapi/ and stores local audit/workflow state in a SQLite file
under the system temp directory.
Read Commands
ibkr-agent account summary --account DU1234567 --json
ibkr-agent portfolio snapshot --account DU1234567 --json
ibkr-agent positions list --account DU1234567 --json
ibkr-agent contracts search AAPL --asset-class stock --currency USD --exchange SMART --json
ibkr-agent contracts resolve AAPL --asset-class stock --currency USD --exchange SMART --json
ibkr-agent market snapshot --contract-id 265598 --json
ibkr-agent market bars --contract-id 265598 --duration "1 D" --bar-size "5 mins" --json
ibkr-agent orders list --account DU1234567 --json
ibkr-agent orders status --account DU1234567 --broker-order-id 123 --json
ibkr-agent executions list --account DU1234567 --json
Feature-Gated Workflows
Order preview, paper order lifecycle, remote MCP, sidecar relay, provider compatibility, and live safety gates now exist in the package. They remain fail-closed and require explicit flags or configuration.
Use:
Validation
cargo fmt --check
cargo clippy --workspace --all-targets --features unstable-internal-test-support -- -D warnings
cargo test --workspace --features unstable-internal-test-support
Interactive Brokers Client Portal Gateway
Retail Client Portal Gateway authentication is handled outside this project by the local Interactive Brokers gateway process.
The local gateway reports whether the broker session is usable, requires manual action, expired, or unavailable. It must not return broker cookies, credentials, raw headers, local secret paths, or raw session material to CLI, MCP, logs, or audit records.
Session States
Session and keepalive outcomes map into safe statuses:
usable: read-only account discovery may proceed.manual_action_required: the user must complete or refresh broker login outside this gateway.unavailable: the local Client Portal Gateway cannot be reached.
The fake backend covers connected, missing-session, expired-session, keepalive-success, and keepalive-expired fixtures so session behavior can be validated offline before any live broker session is used.
Contextual Read Endpoints
The contextual read adapter includes fixture-backed and wiremock-covered paths for options, greeks, market depth, scanners, news, fundamentals, calendar/session, FX, and transfer history. These tests lock the gateway client contract only. Interactive Brokers can vary Client Portal Gateway endpoint availability and naming by Gateway build and entitlement, so validate every contextual read against the exact deployed IBKR Gateway before relying on it in production.
Troubleshooting
- If
backend statusreports manual action required, complete or refresh the broker login in the Interactive Brokers Client Portal Gateway UI. - If keepalive fails, broker-backed read tools must be treated as unavailable until manual login is restored.
- If the local gateway is unreachable, check that the Client Portal Gateway process is running and that the configured URL is local.
verify_tls=falseis only valid for the exact hostslocalhost,127.0.0.1, or::1; wider loopback ranges and IPv4-mapped IPv6 addresses are intentionally not treated as local by the config validator.- Broker cookies, raw headers, tokens, credential file paths, and raw session material must never appear in CLI, MCP, logs, fixtures, or audit output.
For offline checks, run the fake fixture test suite with:
cargo test --workspace --features unstable-internal-test-support
CLI and Tool Commands
This document lists the developer-facing CLI flows. Without --config, the CLI
uses fake fixtures and a local SQLite audit/state file under the system temp
directory. With --config, a missing or invalid file fails closed.
Health, Session, and Accounts
ibkr-agent health --json
ibkr-agent backend status --json
ibkr-agent session requirements --json
ibkr-agent accounts list --json
Session and backend outputs must not include cookies, bearer tokens, raw headers, local secret paths, or raw broker session material.
Portfolio, Positions, Contracts, and Market Data
ibkr-agent account summary --account DU1234567 --json
ibkr-agent portfolio snapshot --account DU1234567 --json
ibkr-agent positions list --account DU1234567 --json
ibkr-agent contracts search AAPL --asset-class stock --currency USD --exchange SMART --json
ibkr-agent contracts resolve AAPL --asset-class stock --currency USD --exchange SMART --json
ibkr-agent market snapshot --contract-id 265598 --json
ibkr-agent market bars --contract-id 265598 --duration "1 D" --bar-size "5 mins" --json
Account-scoped commands require an explicit account id. Contract resolution fails closed when matches are ambiguous. Market data carries status so delayed, stale, or unavailable data can be labeled or refused by policy.
Read-Only Orders and Executions
ibkr-agent orders list --account DU1234567 --json
ibkr-agent orders status --account DU1234567 --broker-order-id 123 --json
ibkr-agent executions list --account DU1234567 --json
These commands inspect existing broker records only.
MCP-Only Maturity Reads
The MCP registry also exposes consultative and safety read tools when their scopes are present:
| Tool | Scope |
|---|---|
ibkr_pnl_daily | ibkr:portfolio:read |
ibkr_pnl_realtime | ibkr:portfolio:read |
ibkr_orders_history | ibkr:orders:read |
ibkr_account_metadata | ibkr:accounts:read |
ibkr_kill_switch_status | ibkr:health:read |
ibkr_limits_status | ibkr:risk:read |
ibkr_audit_export | ibkr:audit:export |
ibkr_session_renew | ibkr:health:read |
These tools are read-only or operational visibility tools. They do not submit, cancel, modify, or approve broker orders.
Advanced market research tools are also MCP-only in this maturity increment:
| Tool | Scope |
|---|---|
ibkr_options_chain | ibkr:options:read |
ibkr_option_greeks | ibkr:options:read |
ibkr_market_depth | ibkr:marketdata:depth:read |
ibkr_scanner_run | ibkr:scanner:read |
Contextual account and market tools are MCP-only and read-only:
| Tool | Scope |
|---|---|
ibkr_news_list | ibkr:news:read |
ibkr_news_article | ibkr:news:read |
ibkr_fundamentals_get | ibkr:fundamentals:read |
ibkr_market_session | ibkr:calendar:read |
ibkr_market_holidays | ibkr:calendar:read |
ibkr_currency_rate | ibkr:currency:read |
ibkr_transfer_history | ibkr:transfers:read |
Order Preview
Preview is non-executable and disabled unless explicitly enabled:
ibkr-agent orders preview \
--account DU1234567 \
--symbol AAPL \
--side buy \
--quantity 1 \
--limit-price 100 \
--currency USD \
--enable-preview \
--json
Without --enable-preview, the command returns ORDER_PREVIEW_DISABLED.
MCP preview additionally accepts stop, stop-limit, and trailing-stop candidates
with the required price or trailing fields for each order type. Market
candidates remain refused by policy.
Paper Orders
ibkr-agent approvals create --account DU1234567 --preview-id <preview_id> --ttl-seconds 300 --json
ibkr-agent orders submit --account DU1234567 --approval-id <approval_id> --idempotency-key paper-submit-001 --enable-paper --json
ibkr-agent orders cancel --account DU1234567 --broker-order-id paper-order-local --idempotency-key paper-cancel-001 --enable-paper --json
Paper submit requires an approval id returned by approvals create for the
specific preview id. Paper submit/cancel/modify require explicit paper
enablement and an idempotency key. Reusing the same key with different canonical request
inputs is refused.
MCP also exposes ibkr_approvals_create under
ibkr:approvals:create. It creates a gateway approval record only for an
existing, unexpired order preview stored by the server and does not represent a
broker-side write.
Live-Gated Candidates
ibkr-agent orders live-submit \
--account DU1234567 \
--approval-id <approval_id> \
--idempotency-key live-submit-001 \
--enable-live \
--live-scope \
--open-kill-switch \
--acknowledge-paper-to-live \
--live-broker local-candidate \
--json
ibkr-agent orders live-cancel \
--account DU1234567 \
--broker-order-id local-candidate-live-submit-001 \
--idempotency-key live-cancel-001 \
--enable-live \
--live-scope \
--open-kill-switch \
--acknowledge-paper-to-live \
--live-broker local-candidate \
--json
Live submit, cancel, and modify run the full gate stack and then call a
LiveOrderWriter. --live-broker selects local-candidate,
client-portal, or refusing; the default returns deterministic
local-candidate-* ids. client-portal requires a config using
broker.backend: client_portal_gateway.
When --config is supplied, live commands use the validated
live_trading.allowed_accounts and live_trading.risk_policy_id from that
runtime config. The CLI invocation flags (--enable-live, --live-scope,
--open-kill-switch, and --acknowledge-paper-to-live) do not add the target
account to the allowlist and cannot replace the configured live policy.
Live submit rate counters and session notional are derived from durable audit
workflow state before the gate stack runs. Caller-supplied
submitted_in_window, submitted_in_session, and instrument context are not
trusted by CLI or MCP live paths.
MCP live modify follows the same server-loaded pattern. The client supplies
approval_id and preview_id for the replacement preview plus bounded changes;
the server reloads the approved order, applies live limits, and consumes the
approval only after a successful writer result.
Bracket Orders
MCP exposes explicit bracket tools for grouped entry, take-profit, and stop-loss
workflows: ibkr_bracket_order_preview,
ibkr_paper_bracket_order_submit, and ibkr_live_bracket_order_submit. See
bracket-orders.md. OCA is not exposed as an MCP tool in
this release.
Audit and MCP
ibkr-agent audit tail --limit 20 --json
ibkr-agent audit export --limit 500 --json
ibkr-agent audit verify --json
ibkr-agent mcp serve --transport stdio --describe --json
ibkr-agent mcp serve --transport stdio
ibkr-agent mcp serve --transport http --describe --enable-remote-mcp --json
ibkr-agent --config config/remote.example.yaml mcp serve --transport http --enable-remote-mcp --bind 127.0.0.1:8080
--describe is a smoke check that prints the selected transport description and
exits. Without --describe, the stdio command runs the local MCP JSON-RPC loop.
It advertises only tools whose scopes are enabled by the current CLI config and
audits every tools/call.
Without --describe, the HTTP command binds the selected address, validates
OAuth/OIDC bearer tokens, serves /.well-known/oauth-protected-resource, and
routes JSON-RPC requests on POST /mcp through the same scoped tool handlers.
CLI audit commands require ibkr:audit:read in the current local scope set and
write their own redacted audit event after tail/export/verify completes.
Remote HTTP MCP and sidecar flows are disabled by default and fail closed without explicit config or flags. See remote-mcp-oauth.md and sidecar-relay.md.
Sidecar Relay
ibkr-agent sidecar identity create --public-key <public-key> --display-name laptop --json
ibkr-agent sidecar pairing create --remote-instance-id remote-1 --sidecar-id sidecar-example --user-id local-user --ttl-seconds 300 --json
ibkr-agent sidecar session create --remote-instance-id remote-1 --sidecar-id sidecar-example --ttl-seconds 300 --json
ibkr-agent sidecar relay accept --remote-instance-id remote-1 --sidecar-id sidecar-example --tool-name ibkr_accounts_list --scope ibkr:accounts:read --payload-json '{}' --json
The sidecar commands expose the relay primitives: identity, pairing, session creation, and sensitive-payload rejection for forwarded requests. They are not a long-running daemon and do not automate Client Portal Gateway login.
MCP
The package exposes provider-neutral MCP tooling for local stdio clients and remote HTTP clients.
Broker authentication remains separate from MCP authorization. MCP bearer tokens must never be forwarded to IBKR.
Local Stdio
ibkr-agent mcp serve --transport stdio --describe --json
ibkr-agent mcp serve --transport stdio --json
Use --describe for a smoke check that exits immediately. Omit it when wiring
an MCP client; the command then runs a line-oriented JSON-RPC stdio loop.
The local server lists only tools whose scopes are enabled by the current CLI
config. Every tools/call enforces the tool scope before backend access and
writes an audit event for completion, denial, refusal, or failure.
Example client configs live under examples/mcp-clients/.
Remote HTTP
Remote MCP is disabled by default and requires explicit configuration plus the independent safety flag. See remote-mcp-oauth.md.
Use --describe for a config smoke check, or omit it to bind the HTTP listener
and serve JSON-RPC requests on POST /mcp:
ibkr-agent mcp serve --transport http --describe --enable-remote-mcp --json
ibkr-agent --config config/remote.example.yaml mcp serve --transport http --enable-remote-mcp --bind 127.0.0.1:8080
The HTTP transport validates OAuth/OIDC bearer tokens, serves protected-resource
metadata, filters tool discovery by granted scopes, and routes authorized
tools/call requests through the same handlers as stdio.
Tool Registry
Read tools:
| Tool | Scope |
|---|---|
ibkr_health | ibkr:health:read |
ibkr_backend_status | ibkr:health:read |
ibkr_session_requirements | ibkr:health:read |
ibkr_session_renew | ibkr:health:read |
ibkr_kill_switch_status | ibkr:health:read |
ibkr_accounts_list | ibkr:accounts:read |
ibkr_account_metadata | ibkr:accounts:read |
ibkr_account_summary | ibkr:portfolio:read |
ibkr_pnl_daily | ibkr:portfolio:read |
ibkr_pnl_realtime | ibkr:portfolio:read |
ibkr_positions_list | ibkr:positions:read |
ibkr_portfolio_snapshot | ibkr:portfolio:read |
ibkr_contracts_search | ibkr:marketdata:read |
ibkr_contract_resolve | ibkr:marketdata:read |
ibkr_market_snapshot | ibkr:marketdata:read |
ibkr_historical_bars | ibkr:marketdata:read |
ibkr_options_chain | ibkr:options:read |
ibkr_option_greeks | ibkr:options:read |
ibkr_market_depth | ibkr:marketdata:depth:read |
ibkr_scanner_run | ibkr:scanner:read |
ibkr_news_list | ibkr:news:read |
ibkr_news_article | ibkr:news:read |
ibkr_fundamentals_get | ibkr:fundamentals:read |
ibkr_market_session | ibkr:calendar:read |
ibkr_market_holidays | ibkr:calendar:read |
ibkr_currency_rate | ibkr:currency:read |
ibkr_transfer_history | ibkr:transfers:read |
ibkr_orders_list | ibkr:orders:read |
ibkr_orders_history | ibkr:orders:read |
ibkr_order_status | ibkr:orders:read |
ibkr_executions_list | ibkr:orders:read |
ibkr_limits_status | ibkr:risk:read |
ibkr_audit_tail | ibkr:audit:read |
ibkr_audit_export | ibkr:audit:export |
Preview and paper tools are discoverable when their scopes are enabled:
| Tool | Scope |
|---|---|
ibkr_order_preview | ibkr:orders:preview |
ibkr_bracket_order_preview | ibkr:orders:preview |
ibkr_paper_order_submit | ibkr:orders:paper:submit |
ibkr_paper_order_cancel | ibkr:orders:paper:cancel |
ibkr_paper_order_modify | ibkr:orders:paper:modify |
ibkr_paper_bracket_order_submit | ibkr:orders:paper:submit |
ibkr_approvals_create | ibkr:approvals:create |
Live tools are discoverable when live scopes are enabled:
| Tool | Scope |
|---|---|
ibkr_live_order_submit | ibkr:orders:live:submit |
ibkr_live_order_cancel | ibkr:orders:live:cancel |
ibkr_live_order_modify | ibkr:orders:live:modify |
ibkr_live_bracket_order_submit | ibkr:orders:live:submit |
Live submit arguments are account_id, approval_id, preview_id, and
idempotency_key. Live modify arguments are account_id, broker_order_id,
approval_id, preview_id, idempotency_key, and at least one bounded
change. The handlers load approval, preview, live policy, writer, market
snapshot, and audit state from the server runtime; these values are not trusted
from the MCP payload. Successful submits are added to the live reconciliation
backlog. Cancel and modify results preserve the broker status, and only
terminal states are removed from pending reconciliation.
Bracket submit tools require three approvals that belong to the same server-persisted bracket preview. Mixed approvals from different preview groups are refused before any paper or live writer boundary. Paper bracket submit also persists durable idempotency state and consumes the three approvals after a successful grouped submit.
The first specs/009-mcp-tool-maturity/ additions are consultative and safety
read tools: PnL, order history, account metadata, kill switch status, live
limits status, MCP audit export, and explicit session renewal. Write-capable
modify additions are explicit (ibkr_paper_order_modify and
ibkr_live_order_modify), while the generic ibkr_order_modify name remains
forbidden.
The same maturity spec adds contextual reads for news, fundamentals, market
session/holiday data, FX rates, and transfer history. ibkr_approvals_create
is a gateway workflow write, not a broker write: it requires
ibkr:approvals:create and an existing unexpired preview persisted by the
server.
Forbidden Generic Write Tools
These generic write-like names remain forbidden:
ibkr_order_intent_validateibkr_order_preview_explainibkr_order_submitibkr_order_cancelibkr_order_modifyibkr_order_approve
Use the explicit preview, paper, or live-gated tools instead. Direct calls to
forbidden names return READONLY_WRITE_FORBIDDEN and are auditable.
Safety Boundary
Tool outputs must not include broker cookies, tokens, credentials, sensitive
headers, local secret paths, or raw Client Portal Gateway session material.
Scope checks happen before broker access. Missing scope returns
AUTH_MISSING_SCOPE and does not call the backend.
Remote MCP OAuth/OIDC
Remote MCP is disabled by default. Enabling it requires both the remote MCP configuration block and the explicit safety flag, so an incomplete deployment fails closed instead of exposing unauthenticated broker tools.
The CLI YAML config uses safety.remote_mcp_enabled. The public Rust
GatewayConfiguration struct exposes the equivalent SDK-facing field as
safety.remote_public_mcp_enabled.
Minimum configuration fields:
remote_mcp.enabled: trueremote_mcp.resource: public protected-resource identifier for the gatewayremote_mcp.issuer: expected OIDC issuerremote_mcp.jwks_url: JWKS endpoint used for token signature checksremote_mcp.audiences: accepted token audiences/resourcesremote_mcp.allowed_scopes: gateway scopes that may be granted remotelyremote_mcp.token_id_hmac_secret_env: environment variable containing the deployment secret used to hash token ids for audit correlationremote_mcp.rate_limit_max_requests: per-client authorization attempts per windowremote_mcp.rate_limit_window_seconds: rate-limit window durationremote_mcp.max_connections: concurrent HTTP connections accepted by the transport before returning503safety.remote_mcp_enabled: true
The CLI HTTP transport is functional when --describe is omitted:
ibkr-agent --config config/remote.example.yaml mcp serve --transport http --enable-remote-mcp --bind 0.0.0.0:8080
It binds the configured address, serves protected-resource metadata at
/.well-known/oauth-protected-resource, and accepts JSON-RPC MCP requests at
POST /mcp. The implementation intentionally uses a small bounded HTTP/1.1
server: it validates Content-Length, caps headers and bodies, returns one
response per connection, caps concurrent connections, applies configured rate
limits, and routes authorized requests through the same tool handlers as stdio.
Broker authentication remains separate from MCP client authorization; MCP bearer tokens must never be forwarded to IBKR.
Example configuration shape:
remote_mcp:
enabled: true
bind_address: 0.0.0.0:8080
resource: https://gateway.example.com/mcp
issuer: https://auth.example.com/
jwks_url: https://auth.example.com/.well-known/jwks.json
metadata_url: https://auth.example.com/.well-known/openid-configuration
audiences:
- https://gateway.example.com/mcp
allowed_scopes:
- ibkr:health:read
- ibkr:accounts:read
- ibkr:portfolio:read
- ibkr:positions:read
- ibkr:marketdata:read
- ibkr:orders:read
- ibkr:audit:read
clock_skew_seconds: 60
rate_limit_max_requests: 120
rate_limit_window_seconds: 60
max_connections: 64
token_id_hmac_secret_env: IBKR_REMOTE_TOKEN_HMAC_SECRET
safety:
remote_mcp_enabled: true
Request behavior:
- missing, malformed, expired, wrong issuer, wrong audience, or bad signature:
401withWWW-Authenticateand protected-resource metadata - valid token with missing tool scope:
403 - repeated authorization attempts from the same forwarded IP or MCP session:
429 - concurrent connections beyond
remote_mcp.max_connections:503 - valid token with the required scope: the request is authorized and the token is not included in downstream broker calls or audit payloads
- valid UUID
x-request-idandmcp-session-idheaders are preserved for request/session correlation; missing or malformed values are replaced with gateway-generated ids initializeandtools/listvalidate the bearer token without a single required tool scope; visible tools are filtered to the token scopes allowed byremote_mcp.allowed_scopestools/callrequires the called tool scope before any backend, order writer, or audit workflow access
Production builds validate RS256 JWTs against RSA JWKS keys. HS256 and JWKS
oct key material are compiled only with the unstable-internal-test-support
feature for deterministic local and CI coverage. Adding ES256 provider keys
should stay inside the OAuth verifier without changing broker-core crates or
tool schemas.
Scopes
Scopes are explicit gateway permissions. Local scopes are loaded from
configuration or test harnesses; remote MCP scopes are granted only after OAuth
token validation and intersection with remote_mcp.allowed_scopes.
IBKR broker authentication is separate from gateway scopes.
Read Scopes
| Scope | Purpose |
|---|---|
ibkr:health:read | health, backend status, session requirements |
ibkr:accounts:read | account discovery |
ibkr:portfolio:read | account summary and portfolio snapshot |
ibkr:positions:read | positions |
ibkr:marketdata:read | contract search/resolve, snapshots, bars |
ibkr:orders:read | read-only orders and executions |
ibkr:audit:read | redacted audit tail |
ibkr:audit:export | redacted audit export |
ibkr:risk:read | risk policy, risk result, and live limit inspection |
ibkr:options:read | options chain and greeks |
ibkr:marketdata:depth:read | bounded Level II/depth reads |
ibkr:scanner:read | allowlisted market scanners |
ibkr:news:read | bounded broker news metadata and articles |
ibkr:fundamentals:read | bounded fundamentals reports |
ibkr:calendar:read | holidays and market session status |
ibkr:currency:read | read-only FX rates |
ibkr:transfers:read | redacted transfer history |
Preview, Paper, Approval, and Live Scopes
| Scope | Purpose |
|---|---|
ibkr:orders:preview | non-executable order preview |
ibkr:orders:paper:submit | paper submit lifecycle |
ibkr:orders:paper:cancel | paper cancel lifecycle |
ibkr:orders:paper:modify | paper order modification lifecycle |
ibkr:approvals:create | MCP-created gateway approval records for existing previews |
ibkr:orders:live:submit | live submit through the live order writer |
ibkr:orders:live:cancel | live cancel through the live order writer |
ibkr:orders:live:modify | live-gated order modification lifecycle |
Preview, paper, and live scopes do not bypass feature flags, approvals, idempotency, risk limits, kill switch, audit availability, or migration checklists.
The local scope-set constructors enforce a tier hierarchy:
ScopeSet::local_with_previewaccepts only read and preview scopes.ScopeSet::local_with_paperaccepts read, preview, paper, and approval scopes, but refuses live scopes withAUTH_SCOPE_NOT_ALLOWED_IN_MVP.ScopeSet::local_with_liveaccepts every local scope, including live ones. Remote OAuth contexts use this constructor to preserve the historical wide remote scope surface.
MCP Tool Mapping
The MCP registry is scope-filtered. Local stdio discovery uses the local scope
set; remote HTTP discovery uses the validated bearer-token scopes after
intersection with remote_mcp.allowed_scopes. Preview, paper, and live tools
are visible only when their explicit scopes are granted, and the runtime gates
still run before any broker write boundary.
| Tool | Minimum scope |
|---|---|
ibkr_health | ibkr:health:read |
ibkr_backend_status | ibkr:health:read |
ibkr_session_requirements | ibkr:health:read |
ibkr_session_renew | ibkr:health:read |
ibkr_kill_switch_status | ibkr:health:read |
ibkr_accounts_list | ibkr:accounts:read |
ibkr_account_metadata | ibkr:accounts:read |
ibkr_account_summary | ibkr:portfolio:read |
ibkr_pnl_daily | ibkr:portfolio:read |
ibkr_pnl_realtime | ibkr:portfolio:read |
ibkr_positions_list | ibkr:positions:read |
ibkr_portfolio_snapshot | ibkr:portfolio:read |
ibkr_contracts_search | ibkr:marketdata:read |
ibkr_contract_resolve | ibkr:marketdata:read |
ibkr_market_snapshot | ibkr:marketdata:read |
ibkr_historical_bars | ibkr:marketdata:read |
ibkr_options_chain | ibkr:options:read |
ibkr_option_greeks | ibkr:options:read |
ibkr_market_depth | ibkr:marketdata:depth:read |
ibkr_scanner_run | ibkr:scanner:read |
ibkr_news_list | ibkr:news:read |
ibkr_news_article | ibkr:news:read |
ibkr_fundamentals_get | ibkr:fundamentals:read |
ibkr_market_session | ibkr:calendar:read |
ibkr_market_holidays | ibkr:calendar:read |
ibkr_currency_rate | ibkr:currency:read |
ibkr_transfer_history | ibkr:transfers:read |
ibkr_orders_list | ibkr:orders:read |
ibkr_orders_history | ibkr:orders:read |
ibkr_order_status | ibkr:orders:read |
ibkr_executions_list | ibkr:orders:read |
ibkr_limits_status | ibkr:risk:read |
ibkr_audit_tail | ibkr:audit:read |
ibkr_audit_export | ibkr:audit:export |
ibkr_order_preview | ibkr:orders:preview |
ibkr_bracket_order_preview | ibkr:orders:preview |
ibkr_paper_order_submit | ibkr:orders:paper:submit |
ibkr_paper_order_cancel | ibkr:orders:paper:cancel |
ibkr_paper_order_modify | ibkr:orders:paper:modify |
ibkr_paper_bracket_order_submit | ibkr:orders:paper:submit |
ibkr_approvals_create | ibkr:approvals:create |
ibkr_live_order_submit | ibkr:orders:live:submit |
ibkr_live_order_cancel | ibkr:orders:live:cancel |
ibkr_live_order_modify | ibkr:orders:live:modify |
ibkr_live_bracket_order_submit | ibkr:orders:live:submit |
Denials
Missing scope returns AUTH_MISSING_SCOPE and emits a denied-scope audit event.
Unknown local or remote scopes fail configuration validation.
Sidecar Relay
The sidecar relay lets a remote MCP gateway route authorized broker requests to a local Client Portal Gateway without exposing IBKR session material remotely.
Required gates for a remote deployment:
- remote MCP OAuth is already enabled and validates the MCP client token
- the local sidecar has an explicit pairing record for the remote instance
- the relay session is bound to the paired sidecar id
- heartbeat is valid and the relay session has not expired
- the local Client Portal Gateway session is usable
The sidecar does not automate retail IBKR browser login. If the local Client Portal Gateway requires authentication, the remote gateway must return a manual local action requirement instead of attempting login automation.
Sensitive material is not forwarded to MCP clients. Forwarded broker requests carry the tool name, required scope, request id, and a payload hash. Cookies, authorization headers, tokens, credentials, secrets, and local filesystem paths are refused before forwarding.
Typical local flow:
ibkr-agent sidecar identity create --public-key <public-key> --display-name laptop --json
ibkr-agent sidecar pairing create \
--remote-instance-id remote-1 \
--sidecar-id sidecar-example \
--user-id local-user \
--ttl-seconds 300 \
--json
ibkr-agent sidecar session create \
--remote-instance-id remote-1 \
--sidecar-id sidecar-example \
--ttl-seconds 300 \
--json
ibkr-agent sidecar relay accept \
--remote-instance-id remote-1 \
--sidecar-id sidecar-example \
--tool-name ibkr_accounts_list \
--scope ibkr:accounts:read \
--payload-json '{}' \
--json
The CLI currently exposes relay primitives for operator and integration workflows: identity creation, pairing records, relay sessions, and request sanitization. It is not a long-running sidecar daemon and it does not persist or heartbeat a local relay process by itself.
Both the CLI YAML config and the SDK GatewayConfiguration model sidecar
enablement as fail-closed. A remote deployment must satisfy all of:
sidecar.enabled: true(CLI YAML or SDK config);safety.sidecar_enabled: true— the independent safety flag is now loaded fromCliConfigFile.safety.sidecar_enabledand the SDKGatewayConfiguration.safety.sidecar_enabled;sidecar.remote_relay_urlandsidecar.local_client_portal_base_urlconfigured;sidecar.heartbeat_timeout_seconds > sidecar.heartbeat_interval_seconds;- remote MCP OAuth already enabled and validated.
If any item is missing the gateway refuses to start with
CONFIG_SIDECAR_FORBIDDEN or CONFIG_INVALID before serving relay traffic.
Provider Compatibility
Provider compatibility is a test and configuration layer around MCP. It does not add provider SDKs to broker, risk, auth, audit, backend, or MCP core crates, and it does not change broker execution semantics.
Supported targets:
- generic MCP inspector over stdio
- Cursor MCP client over stdio
- Continue MCP client over stdio
- OpenAI remote MCP over HTTP with OAuth bearer auth
- Anthropic MCP connector over HTTP with OAuth bearer auth
Local clients use the same stdio command shape:
ibkr-agent mcp serve --transport stdio --describe --json
ibkr-agent mcp serve --transport stdio
Use --describe only for smoke checks. Real local clients should omit it so
the command stays attached to stdio. Local discovery is filtered by enabled
scopes, and every tool call is audited.
Example client configuration files are available under examples/mcp-clients/:
generic-inspector.jsoncursor.jsoncontinue.json
These examples reference IBKR_CONFIG only. They must not embed IBKR
credentials, Client Portal Gateway cookies, OAuth tokens, refresh tokens,
broker session ids, or local absolute paths.
Remote provider connectors should use the remote MCP HTTP endpoint documented
in docs/remote-mcp-oauth.md. The provider receives the gateway protected
resource metadata and sends an OAuth bearer token for the MCP request. The
gateway validates issuer, audience, expiry, signature, and tool scope before
any broker access. MCP bearer tokens are never forwarded to IBKR and are never
written to audit payloads.
Provider-specific behavior belongs in src/internal/provider_compat/ tests or
examples. Core implementation stays provider-neutral; compatibility is proven
through:
- schema snapshots generated from the broker MCP tool registry
- auth denial snapshots for missing token and missing scope
- redaction snapshots for provider-visible outputs and example configs
- dependency checks that forbid provider SDK dependencies in production crates
The provider compatibility harness exercises representative tool discovery, schema, redaction, auth denial, and provider-visible output behavior. Order preview, paper order, and live trading provider flows remain governed by their own policy, approval, scope, idempotency, risk, kill-switch, and audit gates.
Order Preview
Order preview is write-adjacent but still non-executable: the gateway can validate an intent and produce a preview, but it cannot submit, cancel, approve, modify, or create a broker-side order.
CLI
Preview is disabled by default. A local run must opt in explicitly:
ibkr-agent orders preview \
--account DU1234567 \
--symbol AAPL \
--side buy \
--quantity 1 \
--limit-price 100 \
--currency USD \
--enable-preview \
--json
Without --enable-preview, the command returns ORDER_PREVIEW_DISABLED.
MCP
The MCP registry advertises ibkr_order_preview when
ibkr:orders:preview is present in the active local scope set or remote bearer
token grant. The tool uses the same validation and risk path as the CLI and
returns a persisted preview_id for later approval-bound paper or live flows.
The persisted validated order includes the resolved contract id, symbol, and
asset class so live allowlist checks evaluate the actual previewed instrument.
MCP preview accepts limit, stop, stop_limit, and trailing_stop
candidates. Limit candidates require limit_price; stop candidates require
stop_price; stop-limit candidates require both; trailing-stop candidates
require trailing_amount or trailing_percent. Market candidates remain
refused by the default deterministic policy.
Generic submit, cancel, approve, and modify tool names remain forbidden; use the explicit preview, paper, and live-gated tools.
Risk Policy
Risk checks are deterministic. The default policy is disabled and therefore fails closed. Enabled policy checks currently cover account mode, asset class, positive quantity, fractional quantity, quantity limit, order type, required price fields for the selected order type, trailing offsets, and estimated notional.
Audit
Preview work emits preview-phase audit shapes:
order.intent.receivedorder.risk.checkedorder.preview.createdorder.preview.refused
Audit payloads must stay redacted and must not contain broker session material, tokens, cookies, credentials, sensitive headers, or local secret paths.
Paper Orders
Paper orders are the paper-only write workflow. They do not enable live trading.
Required Gates
Paper submit, cancel, and modify require:
- explicit paper trading enablement
- a paper account in the allowlist
- paper submit, cancel, or modify scope
- a persisted approval record for submit
- an idempotency key
- audit events for approval, submit, cancel, modify, and lifecycle transitions
- a configured paper writer for broker-side submit/cancel/modify when validating against Client Portal Gateway
Paper workflows do not enable live trading. Live submit/cancel/modify use the separate live-gated commands and MCP tools, with independent config, scope, approval, risk, kill switch, audit, and paper-to-live gates.
CLI
Create a local approval record:
ibkr-agent approvals create \
--account DU1234567 \
--preview-id <preview_id> \
--ttl-seconds 300 \
--json
Submit a paper order candidate:
ibkr-agent orders submit \
--account DU1234567 \
--approval-id <approval_id> \
--idempotency-key paper-submit-001 \
--enable-paper \
--json
Cancel a paper order candidate:
ibkr-agent orders cancel \
--account DU1234567 \
--broker-order-id paper-order-local \
--idempotency-key paper-cancel-001 \
--enable-paper \
--json
Without --approval-id, paper submit returns PAPER_APPROVAL_REQUIRED.
Approvals are bound to one persisted preview and are consumed after a
successful submit; reuse with a fresh idempotency key returns
APPROVAL_CONSUMED.
Without --enable-paper, paper submit and cancel return a typed disabled
refusal. Approval and idempotency records are persisted in the configured audit
SQLite database so replays remain stable across CLI invocations.
The CLI defaults to LocalCandidatePaperWriter for offline smoke tests. Runtime
deployments can wire ClientPortalPaperWriter to exercise the real paper
account path through the Client Portal Gateway before live trading is enabled.
MCP
The MCP registry advertises explicit paper tools when their scopes are present in the active local scope set or remote bearer token grant:
| Tool | Scope |
|---|---|
ibkr_paper_order_submit | ibkr:orders:paper:submit |
ibkr_paper_order_cancel | ibkr:orders:paper:cancel |
ibkr_paper_order_modify | ibkr:orders:paper:modify |
Paper submit still requires a persisted approval for a preview, an idempotency
key, and paper trading enablement. Paper cancel requires an idempotency key and
paper cancel scope. Paper modify requires account_id, broker_order_id,
idempotency_key, and at least one bounded change (quantity, limit_price,
stop_price, time_in_force, trailing_amount, or trailing_percent).
Generic ibkr_order_submit, ibkr_order_cancel, ibkr_order_modify, and
ibkr_order_approve remain forbidden.
Idempotency
Paper submit/cancel/modify requests must include idempotency keys. Replaying the same
key with the same canonical request is treated as replay. Reusing the same key
with a different request is refused with PAPER_IDEMPOTENCY_CONFLICT.
Before the broker writer is called, the gateway stores a pending idempotency
record. If the process crashes before the final receipt is recorded, the same
key refuses retry until recovery resolves the pending broker-side state.
At CLI startup, pending submit records are recovered by checking broker order
status with the original idempotency key, which is sent to IBKR as cOID.
Broker Response Mapping
Paper writer receipts drive the persisted lifecycle status rather than fixed
values. Paper submit maps Rejected/Refused/Inactive broker statuses to
Refused; paper cancel and modify refuse with BROKER_RESPONSE_INVALID when
the broker did not accept the request and the broker-reported status is not
terminal. Successful order workflow completion (idempotency record, live
reconciliation backlog when applicable, and approval consumption) is committed
in a single SQLite transaction, so a crash window cannot leave an approval in
Approved state after a successful submit.
Provider Approval UX
Provider-side approval prompts are optional client UX only. They may help a human understand what an agent is about to request, but they are not a security boundary for this gateway.
The gateway must still enforce its own gates before broker access:
- OAuth bearer validation for remote MCP clients
- explicit tool scope checks
- local safety flags for disabled feature classes
- order preview policy and non-executable preview records
- paper order approval records and idempotency checks
- audit events for allowed and refused operations
- redaction of tokens, cookies, credentials, headers, and local paths
A provider approval prompt cannot grant missing gateway scopes, enable disabled config, authorize paper trading, or permit live trading. If a provider says a tool call was approved but the gateway policy refuses it, the gateway refusal wins and returns the stable error code for that gate.
Provider UX should display gateway-visible facts only: tool name, required scope, user-facing parameters, preview or approval id when one exists, and the stable refusal or success shape. It must not display or persist broker cookies, OAuth bearer tokens, Client Portal Gateway session material, raw headers, credentials, or local filesystem paths.
Any future provider-specific approval integration must remain outside broker core and should live in compatibility examples or adapters. Broker, risk, approval, audit, auth, and MCP core semantics stay provider-neutral.
MCP-native ibkr_approvals_create creates a gateway approval record, not a
provider UI approval. The tool is scoped with ibkr:approvals:create, loads the
preview from server storage, rejects missing, mismatched, or expired previews,
and then persists the approval to the audit database. Provider prompts may
explain that action to a human, but they cannot replace the stored gateway
approval record consumed by paper and live submit flows.
Paper-to-Live Checklist
Live trading remains disabled until the operator acknowledges this checklist in configuration and the live request supplies every runtime gate.
For the explicit split between items the gateway enforces mechanically (config validation, runtime gates) and items the operator must verify in the deployed environment, see the “Code-Enforced Gates vs Operator-Verified Checks” subsection of production-readiness.md.
Before setting live_trading.enabled: true and
safety.live_trading_enabled: true:
- run the full paper submit/cancel flow with the same account family
- verify approval records are one-use, unexpired, and account-matched
- configure a live account allowlist with only intended live accounts
- configure a live risk policy id and hard limits for notional, quantity, symbol, asset class, frequency, and session exposure
- test the kill switch close/open path
- verify audit storage is writable and live retention is configured
- review the live incident runbook
Live submit requires all of:
- explicit live config and independent safety flag
- allowlisted live account
- live submit scope
- unexpired validated order preview with resolved symbol and asset class
- matching approval record
- idempotency key
- passing live risk policy
- open kill switch
- audit availability
- acknowledged paper-to-live checklist
If any item is missing, the gateway refuses before broker execution.
Live Trading Runbook
Live trading is an operator-controlled mode. It is not enabled by default and must be reversible at runtime through the kill switch.
Enablement
- Complete
docs/paper-to-live.md. - Configure
live_trading.enabled: true. - Add only intended live accounts to
live_trading.allowed_accounts. - Set
live_trading.risk_policy_idto the deployed live policy. - Set
live_trading.paper_to_live_checklist_acknowledged: true. - Set
safety.live_trading_enabled: true. - Confirm audit retention keeps immutable live write events for at least 2555 days and requires export before purge.
Emergency Disable
Close the live kill switch immediately when an unexpected order, policy gap, broker session issue, audit failure, or operator uncertainty appears. A closed kill switch refuses live submit, cancel, modify, and bracket submit before broker execution.
After emergency disable:
- keep audit storage intact
- record the operator, timestamp, reason, request ids, account id hash, and affected broker order ids
- stop provider or MCP clients that initiated the flow
- review the last successful preview, approval, submit, cancel, modify, bracket, and audit events
- reopen live trading only after limits, scopes, approvals, and audit have been verified again
Live Modify
ibkr_live_order_modify is a bounded adjustment path for existing broker
orders. It avoids cancel-and-resubmit races, but it is still a live write: the
handler requires the live modify scope, enabled live config, allowlisted
account, open kill switch, audit availability, and acknowledged paper-to-live
checklist. The MCP payload carries only account_id, broker_order_id,
approval_id, preview_id, idempotency_key, and bounded changes. The
approval must reference the replacement preview loaded by the server; modify
requests without that approval path fail before the writer boundary. Empty
modify requests are also rejected before pending idempotency state is written.
Live Bracket
ibkr_live_bracket_order_submit is an MCP-only grouped write for parent,
take-profit, and stop-loss legs. It requires approved server-persisted previews
for all three legs and rejects approvals mixed from different bracket preview
groups. It evaluates live limits per leg, inserts durable pending idempotency
state before the writer boundary, and consumes all three approvals after a
successful result. The bundled group writer submits legs sequentially through
the configured LiveOrderWriter; it is not broker-native OCA atomicity.
Incident Review Template
- Incident timestamp:
- Operator:
- Account id hash:
- Tool name:
- Request id:
- Approval id:
- Idempotency key:
- Broker order id:
- Kill switch state before incident:
- Kill switch state after incident:
- Live limit policy id:
- Refusal or execution status:
- Audit event ids:
- Root cause:
- Follow-up changes:
Production Readiness
This checklist is for operators and developers preparing a real deployment of
ibkr-agent-gateway.
The package is intentionally conservative, but it is still unofficial software for financial workflows. Treat production enablement as an explicit deployment decision, not as a default mode.
Current Deployment Status
Ready for production-like validation:
- SDK facade with fake and Client Portal Gateway backend constructors;
- CLI runtime config loading for fake or Client Portal Gateway backends;
- read-only broker data paths;
- redacted audit storage and export;
- local MCP stdio serving with scope-filtered tool discovery and audited calls;
- remote MCP OAuth/OIDC validation primitives;
- preview, paper, sidecar, provider compatibility, and live-gate domain logic;
- live MCP submit/cancel handlers that load approval, preview, policy, writer, market snapshot, and audit state server-side;
- live MCP modify and bracket handlers that require approved replacement/group previews, live limit checks, durable pending idempotency, and approval consumption before they are considered complete;
- mature MCP read surface for PnL, order history, account metadata, options, greeks, depth, scanners, news, fundamentals, market sessions, FX rates, and transfer history;
- MCP approval creation for existing unexpired previews, scoped separately from provider UI prompts;
- live order lifecycle reconciliation with a SQLite pending-order backlog;
- live order writer trait with a bundled Client Portal Gateway implementation that returns broker-generated order ids and handles the IBKR reply-chain confirmation protocol.
Live submit, cancel, modify, and bracket flows delegate broker writes to a
LiveOrderWriter implementation
or group writer built from it:
ClientPortalLiveWriterfor production deployments behind a real Client Portal Gateway;LocalCandidateLiveWriterfor CLI smoke tests and offline development — it returns a deterministic local identifier and performs no network I/O;RefusingLiveWriteras a fail-closed default for environments intentionally kept out of broker execution.
The CLI orders live-submit --enable-live /
orders live-cancel --enable-live commands default to
LocalCandidateLiveWriter for offline smoke tests. Operators can select
--live-broker client-portal to use ClientPortalLiveWriter with a
configured Client Portal Gateway backend, or --live-broker refusing for
fail-closed checks. MCP live modify uses the configured live writer. MCP live
bracket uses SequentialLiveOrderGroupWriter, which delegates each leg to the
configured live writer and does not claim broker-native OCA atomicity.
Hard Prerequisites
Before exposing any non-local workflow:
-
run the full validation suite:
cargo fmt --check cargo clippy --workspace --all-targets --features unstable-internal-test-support -- -D warnings cargo test --workspace --features unstable-internal-test-support cargo test --workspace --features unstable-internal-test-support secret cargo doc --workspace --no-deps -
verify the exact binary/library artifact that will be deployed;
-
configure audit storage and verify writes, tail reads, and exports;
-
run
ibkr-agent audit verifyagainst the target audit DB to confirm the chained HMAC log is intact; -
verify CLI
--configloading with a missing-path negative test and a real config smoke test; -
supply stable deployment HMAC secrets from a secret manager;
-
confirm no broker cookies, bearer tokens, credentials, raw headers, local paths, raw account ids, or Client Portal Gateway session material appear in CLI, MCP, logs, fixtures, or audit output;
-
keep broker authentication separate from MCP/OAuth authorization.
Required Secrets
Use deployment-specific secrets. Do not commit them.
| Secret | Purpose |
|---|---|
IBKR_AUDIT_HMAC_SECRET | stable HMAC key for account/audit correlation |
IBKR_REMOTE_TOKEN_HMAC_SECRET | HMAC key for remote token-id audit hashes |
Secrets should be at least 32 random bytes. Rotate only with a plan for audit correlation discontinuity.
Read-Only Broker Deployment
For real broker reads:
- run the Interactive Brokers Client Portal Gateway locally or in the intended private network boundary;
- complete IBKR authentication outside this gateway;
- use
GatewayConfig::client_portal(url)or a verified runtime config path; - allow
verify_tls=falseonly for localhost URLs; - test session required, expired, unavailable, and keepalive behavior before using account or market-data tools;
- validate the richer Spec 009 Client Portal mappings for options, scanners, news, fundamentals, calendar/session, FX, and transfers against the exact deployed Client Portal Gateway version before relying on them operationally. The repository has fixture-backed mapper coverage and wiremock contracts for the expected CPAPI paths/query parameters, but broker endpoint availability can vary by IBKR deployment and entitlement.
Remote MCP
Remote MCP requires:
remote_mcp.enabled: true;safety.remote_mcp_enabled: truein CLI YAML config, orsafety.remote_public_mcp_enabled: truewhen constructingGatewayConfigurationdirectly from Rust;- HTTPS issuer and JWKS URLs;
- RS256 JWTs against RSA JWKS keys in production builds;
- accepted audiences/resources;
- explicit allowed gateway scopes;
remote_mcp.token_id_hmac_secret_envin CLI YAML config, or a populatedRemoteMcpConfig::token_id_hmac_secretin SDK config;- configured gateway rate limiting with
remote_mcp.rate_limit_max_requestsandremote_mcp.rate_limit_window_seconds; - a bounded connection cap with
remote_mcp.max_connections, plus upstream connection limits for internet-facing deployments.
Remote MCP bearer tokens must never be forwarded to IBKR or stored raw in audit.
Sidecar Relay
Use sidecar relay only when:
- remote MCP is already validated;
- a sidecar identity and pairing record exist;
- heartbeat and relay session binding are verified;
- forwarded payloads contain hashes and safe tool metadata only;
- local Client Portal Gateway login still happens manually outside the gateway.
Preview and Paper Trading
Order preview is non-executable. It must remain useful for validation without creating broker-side state.
Paper submit/cancel/modify require:
- explicit paper enablement;
- paper scopes;
- persisted approval records;
- idempotency keys;
- audit availability;
- refusal tests for disabled config, missing approval, and idempotency conflicts.
MCP-created approvals require ibkr:approvals:create and an existing unexpired
server-side preview. Provider approval prompts remain display-only UX and do not
replace gateway approval records.
Live Trading
Do not enable live trading until all items in paper-to-live.md and live-runbook.md are satisfied.
Code-Enforced Gates vs Operator-Verified Checks
The gateway enforces most of the live readiness contract mechanically. The remaining items require deployment-side validation that no library can perform on the operator’s behalf.
Config-validated at startup — the gateway refuses to start otherwise:
live_trading.enabled: true;safety.live_trading_enabled: true— an independent flag deliberately separated fromlive_trading.enabledso a single misconfigured value cannot unlock live trading on its own. The gateway refuses if one is set without the other;live_trading.allowed_accountsis non-empty;live_trading.risk_policy_idis set;live_trading.paper_to_live_checklist_acknowledged: true;audit.live_write_retention_days >= 2555(see audit-retention.md).
Runtime gates evaluated on every live submit/cancel/modify/bracket — the request is refused before the writer is invoked:
- live submit, cancel, modify, or bracket submit scope granted;
- target account present in
live_trading.allowed_accountsloaded from the validated runtime configuration; - approval record one-use, unexpired, account-matched;
- idempotency key present (forwarded to the broker as
cOIDfor broker-side de-duplication); - validated order preview not expired;
- live risk policy passes for the approved order or bracket legs (notional, quantity, symbol, asset class, frequency, session exposure, price collar, and quote freshness);
- live frequency/session counters and session notional are derived from durable
audit workflow state before risk evaluation, not trusted from caller input;
notional counters are deterministic limit-price exposure counters, so market,
stop, and trailing-stop orders without
limit_priceare covered by order count, quantity, symbol/asset-class, price-collar, and quote-freshness gates rather than session-notional arithmetic; - kill switch open;
- audit storage available;
- paper-to-live migration checklist acknowledged on the request
(
paper_trading_validated,approvals_reviewed,limits_reviewed,kill_switch_tested,incident_runbook_reviewed).
Operator-verified before flipping the safety flag — no library can check these for you:
- run the full paper submit/cancel/modify flow against the same account family;
- close and reopen the kill switch in the deployed environment and confirm refusal during the closed window;
- confirm the audit storage actually writes to its target volume and that retention export is automated before purge (the 2555-day floor is validated by config, but the export pipeline is your responsibility);
- validate
ClientPortalLiveWriteragainst your IBKR paper environment before promoting to a live account; - review and rehearse live-runbook.md emergency procedures with the on-call operator.
Close the kill switch on uncertainty.
Live order writer wiring
When the gates pass, the live flow delegates the broker call to the configured
writer boundary. CLI deployments select the bundled Client Portal writer with
--live-broker client-portal and a broker.backend: client_portal_gateway
config. Library consumers should treat the public LiveOrderWriter trait as
the stable extension point; the bundled Client Portal adapter remains an
internal CLI/runtime adapter unless it is promoted through src/public/*.
Live submit, modify, and bracket submit also require a server-side
LivePolicyRegistry. The request only names live_trading.risk_policy_id; the
gateway loads the corresponding LiveLimitPolicy from trusted runtime
configuration before evaluating limits. The live policy should keep
max_price_deviation_bps and max_quote_age_seconds enabled so live writes
refuse stale or out-of-band quotes instead of relying only on notional limits.
Successful live submits and non-terminal live modifies are stored in the SQLite
live_orders_pending backlog.
The MCP runtime polls IbkrBackend::order_status on
live_trading.reconciler_interval_seconds, records lifecycle transitions, and
removes terminal orders from the backlog. Startup also rebuilds the backlog
from completed live idempotency records so non-terminal orders remain tracked
after a restart.
The bundled Client Portal writer:
- posts orders to
/iserver/account/{accountId}/orderswithcOIDset to the idempotency key for broker-side de-duplication; - handles the reply chain (
POST /iserver/reply/{replyId}) up to a configurable depth (default 5) so warning prompts are confirmed once; - refuses market orders and missing limit prices at the writer boundary, in addition to the upstream risk gates;
- returns the broker-generated order id or status in the lifecycle record;
- maps
401toBROKER_SESSION_REQUIRED, transport failures toBROKER_BACKEND_UNAVAILABLE, and oversized responses toBROKER_RESPONSE_INVALID.
Validate the writer in a paper environment before promoting to live. The
contract test suite
(tests/contract_cpapi_live_writer.rs) covers the happy path, reply
confirmation, depth limit, market/limit refusals, 401, decimal
serialization, broker error fields, and cancel response parsing.
Contextual read CPAPI contracts
(tests/contract_cpapi_contextual_reads.rs) cover the expected paths and query
parameters for options, greeks, market depth, scanners, news, fundamentals,
calendar/session, FX, transfer history, and encoded query values. Treat those
paths as gateway adapter contracts, not proof that every endpoint exists in the
currently deployed IBKR Client Portal Gateway build; verify contextual reads
against the exact IBKR Gateway version and entitlements before enabling them for
production operations.
Package Publication
Pre-Publish Runbook
Run the full gate suite, confirm the tarball contents, then tag and publish:
# 1. Quality gates
cargo fmt --check
cargo clippy --workspace --all-targets --features unstable-internal-test-support -- -D warnings
cargo test --workspace --features unstable-internal-test-support
cargo test --workspace --features unstable-internal-test-support secret
cargo doc --workspace --no-deps
cargo audit
# 2. Tarball contents
cargo package --allow-dirty --no-verify --list
cargo publish --dry-run --locked
# 3. Tag and publish
git tag -a vX.Y.Z -m "vX.Y.Z — release notes"
git push origin vX.Y.Z
cargo publish --locked
Tarball Contents
The package must not include local agent directories, editor state, build
outputs, private configs, broker session files, tokens, or machine-specific
paths. The include list in Cargo.toml is the source of truth — it
enumerates exactly what ships and fails closed for everything else.
Verify after every change that touches the repo root:
cargo package --allow-dirty --no-verify --list | head
Audit Log
The gateway records security-relevant activity as redacted, append-only SQLite rows. Audit is used for read operations, scope denials, preview/risk decisions, paper lifecycle transitions, remote auth events, sidecar forwarding, live submit/cancel/modify and bracket lifecycle events, and reconciled live lifecycle transitions.
Storage
The SQLite schema lives under src/internal/audit/migrations/. Live order
reconciliation adds a live_orders_pending backlog keyed by account id and
broker order id.
SqliteAuditWriter configures WAL journaling, writes redacted payload JSON, and
stores a chained HMAC hash for tamper-evidence across appended rows.
Review and Export
ibkr-agent audit tail --limit 100 --json
ibkr-agent audit tail --database-url sqlite:/path/to/audit.db --limit 100 --json
ibkr-agent audit export --database-url sqlite:/path/to/audit.db --limit 500 --json
ibkr-agent audit verify --json
ibkr-agent audit verify --database-url sqlite:/path/to/audit.db --hmac-secret-env IBKR_AUDIT_HMAC_SECRET --json
MCP clients use ibkr_audit_tail with ibkr:audit:read.
CLI audit tail, audit export, and audit verify are also scope-gated by
ibkr:audit:read when a runtime config is supplied, and each command appends a
redacted audit event for the audit read/verification action itself.
audit verify scans the full chained HMAC log and exits with code 2 when
the chain is broken. External database verification requires the original audit
HMAC key through --hmac-secret-env; the runtime database uses the active CLI
configuration key automatically.
Redaction
Audit payloads must not store:
- bearer tokens;
- cookies;
- credentials;
- sensitive headers;
- local secret paths;
- raw Client Portal Gateway session material;
- raw account ids.
Account and token correlation use HMAC-SHA256. Free-form audit metadata is
scrubbed by sensitive field name before persistence. Field-name matching is
case-insensitive and substring-based, so broad markers such as path and
header intentionally redact conservative matches rather than risk leaking
local paths or sensitive headers.
Denied, refused, failed, and completed operations keep a consistent correlation shape so review can reconstruct what happened without exposing broker secrets.
Live Reconciliation
Successful live submits and non-terminal live modifies are added to the
reconciliation backlog. The MCP stdio runtime calls
reconcile_live_orders_once on the configured interval
(live_trading.reconciler_interval_seconds, default 5) to poll
IbkrBackend::order_status, append live_order_lifecycle_changed events on
status transitions, and remove filled/cancelled/refused orders from the
backlog. On startup, the runtime also rebuilds the backlog from completed live
idempotency records so existing non-terminal live orders remain tracked after a
restart.
The same durable live idempotency records provide server-side frequency and session counters for live submit gates. CLI and MCP submit paths overwrite the caller context counters before evaluation.
Audit Retention and Backup
Audit data is operational evidence. It must remain redacted, immutable for live write workflows, and exportable without raw broker secrets.
Retention
- Read-only, preview, remote-auth, sidecar, provider, and paper events should be retained according to operator policy and storage capacity.
- Live write events must be retained for at least 2555 days.
- Live write events must be immutable after append.
- Purge jobs must require a prior export.
- Exports must include event ids, request/session correlation, tool names, scopes, decisions, result status, stable error codes, input/output hashes, and redaction metadata.
Backup
For SQLite deployments:
- Stop writers or use a consistent SQLite backup mechanism.
- Export recent or full audit records with
ibkr-agent audit export --json. - Store the JSONL payload and file hash together.
- Verify the export contains no raw tokens, cookies, credentials, local paths, broker session material, or raw account ids.
- Store backups in access-controlled storage separate from application logs.
Restore and Replay
Replay must use redacted fixtures only. Restored audit exports are evidence for debugging and regression tests; they are not a broker data cache and must not recreate live broker sessions.
Incident Review
Use this template for refused or unexpected broker-gateway behavior, including remote MCP auth failures, sidecar relay failures, paper order issues, live gate refusals, and live kill switch events.
Immediate Actions
- Close the live kill switch if live trading may be affected.
- Preserve audit storage and avoid destructive cleanup.
- Export relevant audit records as redacted JSONL.
- Record request ids, event ids, approval ids, idempotency keys, and account id hashes only.
- Stop any provider, MCP, or sidecar client that is repeating unsafe requests.
Review Template
- Incident id:
- Time range:
- Summary:
- Affected tools:
- Affected topology:
- Audit event ids:
- Request ids:
- Account id hashes:
- Approval ids:
- Idempotency keys:
- Stable error codes:
- Kill switch state before:
- Kill switch state after:
- Replay fixture path:
- Expected decision:
- Actual decision:
- Root cause:
- Customer/operator impact:
- Follow-up actions:
- Owner:
- Due date:
Replay Expectations
Replay fixtures must contain redacted audit events and expected structured decisions only. They must not contain raw bearer tokens, cookies, credentials, broker session material, local filesystem paths, or live broker dependencies.
Testing
The project is validated through Cargo-discoverable tests, fake Client Portal Gateway fixtures, replay checks, provider snapshots, and local performance budgets.
Required Local Gates
cargo fmt --check
cargo clippy --workspace --all-targets --features unstable-internal-test-support -- -D warnings
cargo test --workspace --features unstable-internal-test-support
cargo test --workspace --features unstable-internal-test-support secret
CI also runs documentation and security workflows.
CPAPI Contracts
Wiremock contract tests lock the Client Portal Gateway HTTP boundary for:
- live and paper writer POST/DELETE/modify requests;
- contextual read paths and query parameters for options, greeks, market depth, scanners, news, fundamentals, market sessions/holidays, FX rates, and transfer history.
Fixture Coverage
Fake CPAPI fixtures under tests/fixtures/cpapi/ cover:
- session usable, missing, expired, keepalive success, and keepalive expiry;
- accounts list;
- portfolio snapshot, PnL, account metadata, and positions;
- stock/ETF contract search and ambiguity;
- live, delayed, and stale market snapshots;
- historical bars;
- read-only orders, order history, order status, and executions;
- options chain, greeks, market depth, scanners, news, fundamentals, market session/holidays, FX rates, and transfer history.
Fixtures must not contain tokens, cookies, credentials, sensitive headers, local secret paths, bearer values, or raw broker session material.
Feature Coverage
The test suite covers:
- CLI contracts for read commands, audit, preview, paper, and live-gated refusals;
- MCP tool discovery, schemas, redaction, keepalive, and scope denials;
- remote OAuth RS256 validation, token redaction, generic auth denials, configurable rate limiting, and connection-cap handling;
- order preview, risk checks, paper approval/idempotency, paper modify, live limits, live modify, bracket submit, kill switch, and paper-to-live gates;
- sidecar identity, pairing, heartbeat, forwarding safety, and secret scans;
- provider compatibility snapshots and provider SDK dependency boundaries.
Replay and Performance
Replay tests check audit redaction and secret-scan behavior. Performance tests assert budgets for fake backend reads, audit append/tail, cached remote OAuth validation, prepared remote MCP authorization, live gate/risk/idempotency, and sidecar request safety.
To measure the full offline suite duration locally:
time cargo test --workspace --features unstable-internal-test-support
The security workflow filters tests by secret while still enabling
unstable-internal-test-support, because several secret/redaction regression
tests intentionally use hidden internal fixtures and helpers.