The control boundary
The live core has exactly one external control surface — commands and order intents in, snapshot queries out — and every front end, in-process or remote, is a translator over it.
crates/vike-core/src/control.rs names the external control boundary of the live core in one
place, and its contract fits in a sentence: Command and OrderIntent go in through
CoreHandle::try_command (non-blocking, the desktop app's path) or CoreHandle::send_command
(lossless, for session control and tests), and CoreSnapshot queries come out through the
snapshot's accessors — orders_for, order, open_order, position, last_mark, equity,
trading_state. The desktop app, the CLI and the MCP server are translators over that seam. Nothing
bypasses it.
Why one seam matters: the risk gate is structural
Every order-scoped verb is an OrderIntent, and every intent from every origin lowers through the
core's single apply_intent site: mint a client-order-id, route, run the RiskGate, hand the order
to the venue client. crates/vike-core/src/runtime/apply.rs lists the origins it serves — the
external command path, a mounted strategy's drain, a conditional order firing, a margin-call
liquidation — and notes that a source-scan test in the control-boundary suite enforces that no engine
submit call lives outside that file. The consequence for a reader is that there is no path by which
a front end can place an order that the risk gate did not see. It
is a property of the code's shape, not a convention each caller has to remember.
Fire-and-forget
Commands have no synchronous reply. The core is a single-writer fold,
and a synchronous order handle was considered and rejected. A caller that must know its
client-order-id pre-mints it: a non-empty client_order_id on the request is respected, and an
empty one asks the core to mint. Outcomes are observed through the snapshot — rejected_commands
counts queue rejections, and a RiskGate veto surfaces as an OrderDenied event in
recent_events. Everything about an order's later life arrives as venue events and is read from the
published snapshot, which is how the
order lifecycle is meant to be watched.
The two entry points differ only in what happens when the ingest lane is full.
CoreHandle::try_command is a try_send: it never blocks, a full queue returns
CommandRejected::Busy and increments the same rejected counter the snapshot surfaces, and a closed
lane — the core has exited — returns CommandRejected::Gone. CoreHandle::send_command is a
blocking_send into the same lane: every command queued ahead of it still folds first, and a closed
lane is ignored, matching shutdown_and_join.
Why the GUI path may drop and the session path may not
crates/vike-core/src/runtime/mod.rs pins the ingest design: one dedicated thread owns all mutable
trading state and reaches it through one totally-ordered queue. Exec events from venues are lossless
and back-pressure the venue task — never the GUI. Commands from the GUI use try_send so the GUI
thread never blocks, and a full-queue rejection is returned to the caller and counted in the
snapshot, so it cannot be silent. Market data rides a latest-wins conflation slot with at most one
marker in flight. The two command paths are the same lane with two policies for the full case.
The out-of-process write half
CoreHandle::command_sink hands out a CommandSink: a narrow, cloneable capability holding only
the ingest lane and the shared rejected counter. It is what the headless daemon's control server
holds, and it lowers a remote Scope::Control peer's order command into the same apply_intent
seam — carrying no snapshot read, no market slot and no join handle, so a connection can neither
read state nor shut the core down. Its try_command is, by its own doc, the exact body of
CoreHandle::try_command, so mint → RiskGate → client stays structural on the network path too.
The sink also carries send_blocking, the lossless twin, for the operator's own stdio loop — a human
at a terminal may wait for their typed order. The remote server path deliberately keeps
try_command: a network peer must never be able to back-pressure a per-connection thread. How a
remote peer reaches the sink at all — keys, framing, the handshake — is the
node protocol.
Building another front end
The module's own guidance for a new adapter is short. In-process — a headless mode or a REPL inside
the running core binary — holds the CoreHandle and the snapshot cell directly; no transport is
needed. Command is already serde, so the recommended first cut for anything else is newline-JSON
over stdio. Out-of-process additionally needs a transport and Serialize on the snapshot view
types; that layer was parked until a real consumer existed, and the write half of it now does, as
the CommandSink above. The CLI's trade verb and the
agent tools are both translators in exactly this sense: they compose commands, submit
them, and read the snapshot.
Portfolio and snapshot
The core publishes one immutable state view through an arc-swap cell that the window reads on repaint and can never back-pressure; the portfolio inside it is the named cross-venue aggregator, with the two totals that must not be printed as one.
Mounting
A run profile names the whole assembly in one auditable file, a strategy mount has a deterministic identity with a durable state sidecar beside it, and runtime mounts survive a restart through a sidecar of their own rather than through the journal.