You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cancelling an open entry order is refusing risk — frictionless by constitution, safe as a one-click web action. Cancelling an open exit or protective order is removing protection — never one click; walled off, explained, and redirected to the typed friction of the terminal. No broker makes this distinction. It falls straight out of keel's constitution (exits degrade to typed friction; refusing entries is always allowed).
Context (verified)
No orders CLI group and no cancel command exist today — cancel appears nowhere in keel/cli.py or keel/commands/. Cancellation lives only inside executor internals: _cancel_at_exchange (keel/execution/executor.py:1668), _clear_resting_bracket (:268), the _roll_stop: a crash between cancel and replace leaves the position naked AND silent #519 cancel-before-place protocol.
The broker port has cancel_order(order_id) -> CancelOutcome (keel/data/cb_client.py:364); the Alpaca adapter implements refused/unknown outcomes.
The web's only write surface today is POST /api/setup/<key> behind: Host check, session cookie, Sec-Fetch-Site, X-Keel-Client: 1, HMAC X-Keel-CSRF (keel/web/security.py:59-71). This issue adds the first non-setup write action — hence its own security pass.
Web: POST /api/orders/cancel behind the full existing guard machinery, plus:
Strict side/role guard: if the target order is side == sell, or is linked as any position's bracket_order_id (protective legs are the real hazard, not merely the side label), respond 403 with a typed payload:
{
"error": "EXIT_PROTECTION_LOCKED",
"message": "Exits and protective brackets cannot be cancelled from the web console. Use 'keel orders cancel <id>' in the terminal."
}
Only open/resting statuses are cancellable; anything else is a named refusal, never a silent no-op.
Security pass: idempotency on repeated cancels, rate sanity, an audit log line per attempt (allowed and refused), and the refusal path tested as hard as the happy path.
Acceptance
Mutant tests: removing the side guard or the bracket-link guard fails the suite; removing the bracket-clearing fails the suite.
403 payload contract pinned by test; CLI typed-phrase path pinned by test.
The security review sign-off lands as a comment here before merge.
Sprint 3, after#698–#705 — it opens a new write surface and borrows their patterns. Explicitly does not wait on #648 (loopback-only; remote exposure is a separate rail).
The principle
Cancelling an open entry order is refusing risk — frictionless by constitution, safe as a one-click web action. Cancelling an open exit or protective order is removing protection — never one click; walled off, explained, and redirected to the typed friction of the terminal. No broker makes this distinction. It falls straight out of keel's constitution (exits degrade to typed friction; refusing entries is always allowed).
Context (verified)
cancelappears nowhere inkeel/cli.pyorkeel/commands/. Cancellation lives only inside executor internals:_cancel_at_exchange(keel/execution/executor.py:1668),_clear_resting_bracket(:268), the _roll_stop: a crash between cancel and replace leaves the position naked AND silent #519 cancel-before-place protocol.cancel_order(order_id) -> CancelOutcome(keel/data/cb_client.py:364); the Alpaca adapter implements refused/unknown outcomes.POST /api/setup/<key>behind: Host check, session cookie,Sec-Fetch-Site,X-Keel-Client: 1, HMACX-Keel-CSRF(keel/web/security.py:59-71). This issue adds the first non-setup write action — hence its own security pass.Scope
keel orders cancel <id>— entry-side orders cancel ony/N; exit-side or protective orders require the typed phrase. Addkeel orders listwhile the group exists (reads the [web] Orders view: the per-order honesty record — expected vs actual fill, fee as paid, provenance #700 surface).POST /api/orders/cancelbehind the full existing guard machinery, plus:side == sell, or is linked as any position'sbracket_order_id(protective legs are the real hazard, not merely the side label), respond403with a typed payload:{ "error": "EXIT_PROTECTION_LOCKED", "message": "Exits and protective brackets cannot be cancelled from the web console. Use 'keel orders cancel <id>' in the terminal." }Acceptance
Sprint 3, after #698–#705 — it opens a new write surface and borrows their patterns. Explicitly does not wait on #648 (loopback-only; remote exposure is a separate rail).