Skip to content
Cirrus home
Sign in

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.

  1. Additive

    Synthetic MCP recordings refreshed

    build. tools/list gains four tools: create_auto_backtest_sweep, get_auto_backtest_sweep, list_auto_backtest_sweeps and cancel_auto_backtest_sweep. list_activity gains the optional input client_id. Descriptions of calculate_order_margins, list_triggers_v2, get_trigger_v2, modify_trigger_v2, cancel_trigger_v2, pause_trigger_v2 and resume_trigger_v2 follow their operations. No tool, input or output field removed or renamed; no input became required; other changes are JSON key order only.

  2. Additive

    `/v2/triggers` for Pocketful and 5Paisa users

    • The pocketful and fivepaisa tenants run the same v2 triggers profile as cirrus (owner decision): every user may call /v2/triggers (entries, risk targets, strategy risk and trigger_update webhooks included), where they answered 403 before. Takes effect when the build is deployed with OMS_TRIGGERS_V2_ENV set (docs/runbooks/trigger-engine-v2-tenants.md).
  3. 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; AutoStrategyPlan gains optional origin_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.
  4. Additive

    Backtest sweeps show in activity; sweep writes take Idempotency-Key

    • POST /v2/backtests/sweeps (create_auto_backtest_sweep) records one backtest_submitted activity (existing kind) with summary Sweep of

    “{name}” v{n} ({k} versions) from {from} to {to} and the caller as its source, as POST /v2/backtests does. An idempotent replay records nothing more.

    • The OpenAPI document declares the optional Idempotency-Key header on POST /v2/backtests/sweeps and DELETE /v2/backtests/sweeps/{sweep_id} (both already honoured it).
  5. Additive

    Connections say whether the app is an MCP client

    • GET /v1/connections: each item gains dynamic (boolean): true when the connection's app registered itself dynamically (an MCP client), false for classic partner apps. The list_partner_connections example shows it.
  6. Additive

    AS metadata lists `offline_access`

    • GET /.well-known/oauth-authorization-server: scopes_supported also lists offline_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.
  7. 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 carries error.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 no reason.

  8. 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 with trading off now makes no write at all: every POST, PUT, PATCH and DELETE is refused with 403 forbidden ("This connection is read-only"), including strategy definitions, backtests (and sweeps), watchlists, postbacks and PUT /v2/signal-modes/{endpoint_id}. Reads and pure checks (POST /v1/margins/orders, POST /v2/strategies/dry-run, the validate routes) stay allowed. POST /v2/strategies/stop-all from 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) gains left_running: how many running deployments were left (omitted when 0, so no other answer changes).

  9. 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 403 forbidden (message names the limit, recorded in activity as failed) when trading is false ("This connection is read-only"), on an unlisted account ("This connection may not trade on {account}") or on a real account when paper_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) and POST /v2/deployments/{deployment_id}/signals with exit / exit_legs. These risk-reducing writes are never valued against max_order_value nor counted against max_orders_per_day. The OpenAPI document lists the 403.

  10. 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 a risk_limit that flattens the user's real accounts. Now POST /v2/triggers, PATCH /v2/triggers/{trigger_id} and POST /v2/triggers/{trigger_id}/resume check what the trigger acts on: a risk_limit at level account (and any trigger) is checked on its account like a leg (trading, accounts, paper only); at level user (or strategy) it acts on every account of the user and is refused with 403 forbidden ("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_only is off and trading is on. A modify is checked at the trigger's current and its requested level. Refusals are recorded in activity as failed. The OpenAPI document says so on these operations.

  11. Additive

    The v1 protection sunset applies per tenant

    • The 30 Dec 2026 sunset of /v1/triggers (the Sunset header and the 410 gone from that instant) applies only on a deployment where /v2/triggers is open to every user: Cirrus. Where /v2/triggers is not open yet (the fivepaisa and Pocketful deployments today) the v1 routes keep working after the date, with Deprecation and Link but no Sunset, until v2 opens there and its users are told the date.
    • Operators can open /v2/triggers per deployment (OMS_TRIGGERS_V2_USERS) and disarm or move the sunset (OMS_V1_PROTECTION_SUNSET) without a release (runbook docs/runbooks/v1-protection-sunset.md).
  12. Additive

    MCP tokens stored apart from API tokens

    • mcp authorization codes, access tokens and refresh tokens are stored under their own keys: Redis oms:oauth:mcp:code:{hash} and oms:oauth:mcp:access:{hash}, Mongo partner_refresh_tokens _id mcp:{hash}. Same record shapes. Every pod reads both places, so api tokens, and any mcp token 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, refresh invalid_grant), never a plain API token without its connection limits (runbook docs/runbooks/mcp.md, "Rollback").
  13. Additive

    Ended v1 records on `/v2/triggers` show v2 states only

    ones, no shape change)

    • GET /v2/triggers and GET /v2/triggers/{trigger_id}: a protection held as a v1 record that ended now shows one of v2's own ended state values, with v1's status or reason in reason. Since "/v2/triggers manages every protection /v1/triggers shows" (2026-10-31), state could 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 stays pending, active or cancel_pending (already v2 states).
  14. Additive

    MCP tokens only for self-registered clients; registration abuse limits

    • GET /v1/oauth/consent and POST /v1/oauth/authorize: a registered partner app that sends resource is refused with 422 invalid_target (RFC 8707; new error code). Only self-registered (MCP) clients connect through the Cirrus MCP and get mcp tokens, so one connection never holds tokens of both kinds and its consent limits bind every token it has. No released client sends resource (the earlier entry that let registered apps do so never shipped); without it nothing changes. default_limits is 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 (429 slow_down with Retry-After past 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}.
  15. Additive

    Disconnect summary lists what was left running only for MCP connections

    • The partner_revoked activity summary Disconnected “{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.

  16. Additive

    Resource-bound authorization for MCP connections

    • GET /v1/oauth/consent and POST /v1/oauth/authorize take an optional resource (RFC 8707): the MCP server URL (a trailing slash is ignored). A self-registered app must send it (422 invalid_request "resource must be <url>" otherwise); a registered partner app may send it to connect through the Cirrus MCP, and its tokens are then mcp tokens (they work only through the MCP, also after a refresh). Without resource a registered app's flow is unchanged.
    • GET /v1/oauth/consent (OAuthConsent) gains optional dynamic (true), redirect_host (host the code is sent to) and vendor (the AI vendor owning the redirect URI), sent only for a self-registered app (vendor only when it is a known vendor's). A registered partner app's consent response is unchanged.
    • POST /v1/oauth/token takes an optional resource; when sent it must be the one the user authorized, else 400 invalid_grant.
    • Stored records (all new fields optional, defaults keep old records readable): authorization codes (Redis, 60 s) gain resource and audience; refresh tokens (Mongo partner_refresh_tokens) gain audience (absent: api); mcp access tokens (Redis) gain redirect_uri; connections (Mongo partner_connections) gain redirect_uri. Readers first: a pod without this build refreshes a registered app's mcp grant as api (what that app could get anyway), so set OMS_MCP_PUBLIC_URL on the api only once every pod runs it.
  17. Additive

    Stricter client names and redirect checks at dynamic client registration

    • POST /v1/oauth/register (register_oauth_client, not yet enabled anywhere): client_name must be printable ASCII, else 400 invalid_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 (MyClaude and Precursor are refused); short ones (xAI, Grok) only at a word boundary (x-AI is refused, Max AI is 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.
  18. Additive

    Connection limits value every order at least at the market

    For mcp-audience partner tokens only: every order, GTT, trigger and modify checked against max_order_value is 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 403 forbidden. App sessions, API keys and classic partners are unchanged.

  19. 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-limits answers 403 forbidden when 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 under max_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.