Changelog
Every change you can see, newest first. Additive changes never break a client that ignores unknown fields; breaking ones are announced before they ship.
- Additive
Synthetic MCP recordings refreshed
build.
tools/listgains four tools:create_auto_backtest_sweep,get_auto_backtest_sweep,list_auto_backtest_sweepsandcancel_auto_backtest_sweep.list_activitygains the optional inputclient_id. Descriptions ofcalculate_order_margins,list_triggers_v2,get_trigger_v2,modify_trigger_v2,cancel_trigger_v2,pause_trigger_v2andresume_trigger_v2follow their operations. No tool, input or output field removed or renamed; no input became required; other changes are JSON key order only. - Additive
`/v2/triggers` for Pocketful and 5Paisa users
- The
pocketfulandfivepaisatenants run the same v2 triggers profile ascirrus(owner decision): every user may call/v2/triggers(entries, risk targets, strategy risk andtrigger_updatewebhooks included), where they answered403before. Takes effect when the build is deployed withOMS_TRIGGERS_V2_ENVset (docs/runbooks/trigger-engine-v2-tenants.md).
- The
- Additive
Cross-user aggregate cap for automated strategies (stored records)
- New Redis records shared by pods:
auto_aggregate_limits,auto_aggregate_share,auto_aggregate_totals;AutoStrategyPlangains optionalorigin_strategy_id(serde default; readers ship first, the API writer follows in a later release). - A capped strategy's new entries wait for every user once its live copies reach the cap (journey note, one notice per strategy per day); open trades are never closed. Uncapped strategies are unaffected. No cap is set anywhere yet.
- New Redis records shared by pods:
- Additive
Backtest sweeps show in activity; sweep writes take Idempotency-Key
POST /v2/backtests/sweeps(create_auto_backtest_sweep) records onebacktest_submittedactivity (existing kind) with summarySweep of
“{name}” v{n} ({k} versions) from {from} to {to}
and the caller as its source, asPOST /v2/backtestsdoes. An idempotent replay records nothing more.- The OpenAPI document declares the optional
Idempotency-Keyheader onPOST /v2/backtests/sweepsandDELETE /v2/backtests/sweeps/{sweep_id}(both already honoured it).
- Additive
Connections say whether the app is an MCP client
GET /v1/connections: each item gainsdynamic(boolean):truewhen the connection's app registered itself dynamically (an MCP client),falsefor classic partner apps. The list_partner_connections example shows it.
- Additive
AS metadata lists `offline_access`
GET /.well-known/oauth-authorization-server:scopes_supportedalso listsoffline_access(MCP clients such as Claude and Codex ask for refresh tokens with it only when listed). It grants nothing more: requesting it is ignored like any unknown scope token, and refresh tokens are issued as before.
- Additive
`error.reason` on connection-limits refusals
- Every route an agent connection's limits guard (orders, GTT, triggers, strategies, deployments): a refusal by those limits stays
403
forbidden
and now also carrieserror.reason: "connection_limits", so a client can tell "this action is over the limits the user set" from "this credential may not do this". Other errors have noreason. - Every route an agent connection's limits guard (orders, GTT, triggers, strategies, deployments): a refusal by those limits stays
- Additive
Read-only connections make no write; stop-all spares other accounts
For
mcp-audience partner tokens only (app sessions, API keys and classic partners are unchanged). A connection withtradingoff now makes no write at all: everyPOST,PUT,PATCHandDELETEis refused with 403forbidden("This connection is read-only"), including strategy definitions, backtests (and sweeps), watchlists, postbacks andPUT /v2/signal-modes/{endpoint_id}. Reads and pure checks (POST /v1/margins/orders,POST /v2/strategies/dry-run, thevalidateroutes) stay allowed.POST /v2/strategies/stop-allfrom a limited connection no longer refuses when some deployments act on accounts it may not act on: it stops only the deployments on accounts it may act on and leaves the others running. The answer (AutoStopAllResult) gainsleft_running: how many running deployments were left (omitted when 0, so no other answer changes). - Additive
Connection limits bound every write to the connection's accounts
added to existing operations) For
mcp-audience partner tokens only (app sessions, API keys and classic partners are unchanged). The 2026-10-30 entry "Cirrus MCP connection limits are enforced" said exits, cancels, pause, stop, exit-now and stop-all were never refused by a limit; that let a read-only or paper-only connection delete a real account's stop-loss or close its positions. Now every write acts only on accounts the connection may act on: refused with 403forbidden(message names the limit, recorded in activity asfailed) whentradingis false ("This connection is read-only"), on an unlisted account ("This connection may not trade on {account}") or on a real account whenpaper_only("This connection may trade only on paper accounts"). Newly gated:POST /v1/positions/exit,DELETE /v1/orders/{order_id},DELETE /v1/orders/bracket/{order_id},DELETE /v1/gtt/{trigger_id},DELETE /v1/triggers/{trigger_id},DELETE /v2/triggers/{trigger_id}(broker-held protection too),POST /v2/triggers/{trigger_id}/pause,POST /v2/deployments/{deployment_id}/pause,/stop,/exit-now,POST /v2/strategies/stop-all(stops only the deployments on accounts the connection may act on and leaves the others running; see the entry above) andPOST /v2/deployments/{deployment_id}/signalswithexit/exit_legs. These risk-reducing writes are never valued againstmax_order_valuenor counted againstmax_orders_per_day. The OpenAPI document lists the 403. - Additive
Connection limits gate triggers without legs (`risk_limit`)
on existing operations) For
mcp-audience partner tokens only (app sessions, API keys and classic partners are unchanged): a trigger without legs was checked against nothing, so a paper-only connection could set arisk_limitthat flattens the user's real accounts. NowPOST /v2/triggers,PATCH /v2/triggers/{trigger_id}andPOST /v2/triggers/{trigger_id}/resumecheck what the trigger acts on: arisk_limitatlevelaccount(and any trigger) is checked on its account like a leg (trading, accounts, paper only); atleveluser(orstrategy) it acts on every account of the user and is refused with 403forbidden("This connection may not act on every account: a user-level rule needs every account listed and paper only off") unless every account of the user is listed,paper_onlyis off andtradingis on. A modify is checked at the trigger's current and its requested level. Refusals are recorded in activity asfailed. The OpenAPI document says so on these operations. - Additive
The v1 protection sunset applies per tenant
- The 30 Dec 2026 sunset of
/v1/triggers(theSunsetheader and the410gonefrom that instant) applies only on a deployment where/v2/triggersis open to every user: Cirrus. Where/v2/triggersis not open yet (the fivepaisa and Pocketful deployments today) the v1 routes keep working after the date, withDeprecationandLinkbut noSunset, until v2 opens there and its users are told the date. - Operators can open
/v2/triggersper deployment (OMS_TRIGGERS_V2_USERS) and disarm or move the sunset (OMS_V1_PROTECTION_SUNSET) without a release (runbookdocs/runbooks/v1-protection-sunset.md).
- The 30 Dec 2026 sunset of
- Additive
MCP tokens stored apart from API tokens
mcpauthorization codes, access tokens and refresh tokens are stored under their own keys: Redisoms:oauth:mcp:code:{hash}andoms:oauth:mcp:access:{hash}, Mongopartner_refresh_tokens_idmcp:{hash}. Same record shapes. Every pod reads both places, soapitokens, and anymcptoken already stored the old way, keep working. A build from before agent access never reads the new keys: after a rollback an agent's token is invalid (401, refreshinvalid_grant), never a plain API token without its connection limits (runbookdocs/runbooks/mcp.md, "Rollback").
- Additive
Ended v1 records on `/v2/triggers` show v2 states only
ones, no shape change)
GET /v2/triggersandGET /v2/triggers/{trigger_id}: a protection held as a v1 record that ended now shows one of v2's own endedstatevalues, with v1's status or reason inreason. Since "/v2/triggers manages every protection /v1/triggers shows" (2026-10-31),statecould carry v1's free-form strings (exit_leg_rejected,signal_exit,broker_squared_off,stop_loss_executed_by_broker,cancelled_by_user,target_reached,invalidated,exit, or""), which a client treating unknown states as live read as live protection. Now:target_hit(target_hit,target_reached,cancelled_for_target);sl_hit(sl_hit,stop_loss_executed_by_broker,triggered,stop_order_filled);fired(fired_at_broker);done, the position was exited another way (exit,signal_exit,broker_squared_off,position_closed,exit_leg_filled,stop_order_cancelled_for_exit);cancelled(cancelled,cancelled_by_user,deleted_by_user,invalidated,broker GTT cancelled,day_ended,contract_expired,exit_leg_cancelled,exit_leg_expired,stop_order_cancelled_exit_not_placed);errored, it stopped protecting because something failed (exit_leg_rejected,stop_order_ended,stop_leg_refused) or why is not known (any other reason, or none). A live v1 record stayspending,activeorcancel_pending(already v2 states).
- Additive
MCP tokens only for self-registered clients; registration abuse limits
GET /v1/oauth/consentandPOST /v1/oauth/authorize: a registered partner app that sendsresourceis refused with 422invalid_target(RFC 8707; new error code). Only self-registered (MCP) clients connect through the Cirrus MCP and getmcptokens, so one connection never holds tokens of both kinds and its consent limits bind every token it has. No released client sendsresource(the earlier entry that let registered apps do so never shipped); without it nothing changes.default_limitsis sent only for self-registered apps.POST /v1/oauth/register(not yet enabled anywhere): an IPv6 caller's hourly budget (10) is its /64, not its single address; registrations from every caller together are capped per hour (429slow_downwithRetry-Afterpast it). A client nobody ever connected through is deleted after 24 hours (was 30 days); one with only ended connections keeps the 30 days.- Mongo indexes added:
partner_connections {client_id},partner_apps {dynamic, created_at}.
- Additive
Disconnect summary lists what was left running only for MCP connections
- The
partner_revokedactivity summaryDisconnected “{name}”. Left
running: {n} open orders, {g} GTT, {t} triggers, {d} strategies.
is now recorded only when the connection is an MCP one (a dynamically registered client). A classic partner app's disconnect is recorded as before that change:Disconnected partner app “{name}”(this restores the old text for classic apps). The revoke response is unchanged. - The
- Additive
Consent carries connection limits into the connection
GET /v1/oauth/consentreturnsdefault_limits(the limits to offer) for a connection through the MCP server; left out otherwise.POST /v1/oauth/authorizetakes an optionallimitsfor such a connection (422invalid_requestwhen they contradict themselves, name an account that is not the user's, or ask forlive_strategieswithout thestrategies_livescope; also when sent for any other connection). Left out: the defaults. Every consent replaces the connection's earlier limits.
- Additive
Stricter client names and redirect checks at dynamic client registration
POST /v1/oauth/register(register_oauth_client, not yet enabled anywhere):client_namemust be printable ASCII, else 400invalid_client_metadata(RFC 7591 §3.2.2). A name carrying a known AI vendor's brand is checked against every redirect URI that is not loopback http or a known desktop app scheme, whatever case its scheme is written in. Brands of 5+ letters match anywhere in the name (MyClaudeandPrecursorare refused); short ones (xAI,Grok) only at a word boundary (x-AIis refused,Max AIis not). The 400 message says the name contains a reserved brand. Redirect URIs are stored and returned as parsed (scheme and host lower-cased); either spelling matches at consent and token.
- Additive
Connection limits value every order at least at the market
For
mcp-audience partner tokens only: every order, GTT, trigger and modify checked againstmax_order_valueis valued at the higher of its own price and the instrument's last price (a limit order priced far from the market fills at the market). Without a last price the request is refused with 403forbidden. App sessions, API keys and classic partners are unchanged. - Additive
Connection limits gate overall risk limits; modifies are valued at least at the market
For
mcp-audience partner tokens only:PUT /v2/strategies/risk-limitsanswers 403forbiddenwhen the connection may not run strategies live or is limited to paper accounts (it changes limits that live strategies rely on). Modifies of a bracket, GTT, trigger or protection are valued at no less than the instrument's last price, whatever price the request names, so a tiny price cannot slip undermax_order_value; without a last price the modify is refused. App sessions, API keys and classic partners are unchanged. The OpenAPI document lists the 403.