Agent safety: Emergency Stop and Isolated Session Provisioning

Two related safety features for MCEC’s Agent Mode. When MCEC is agent-driving the desktop, the target app has focus, not MCEC; so the operator needs (a) a way to instantly intervene when a run goes wrong, and (b) assurance that a session can’t leave the machine in an unsafe state. Both features exist so the operator stays in control.


1. Emergency Stop: a global dead-man’s-switch hotkey

Goal

A single keystroke, hittable from any focused window, that instantly halts the active agent session; the human override. The agent must not be able to trip it accidentally or defeat it deliberately.

The hotkey

What “stop” does

Engaging the stop (EmergencyStop.Trigger):

  1. Latches the actuation gate. AgentRuntime.EmergencyStopped is set; AgentServer.CallTool refuses every tool call (actuation, observation, and raw send_command) with the distinct emergency-stopped error until re-armed.
  2. Aborts in-flight actuation. Any recording is stopped; an in-flight drag observes the latch between waypoints and bails out (releasing the button). The invoke modal-grace worker runs a synchronous UIA call that can’t be safely aborted mid-flight, but the latch refuses every follow-up and the input release neutralizes anything left held.
  3. Releases held input. All mouse buttons up; shift/ctrl/alt/win reset (the same reset MainWindow runs on exit); no stuck drag or chord.
  4. Loud feedback. A persistent red ⛔ STOPPED by operator banner on the overlay (replacing the routine centered MCEC is controlling your PC banner it shows while running), an AGENT-AUDIT: log line, a status-bar message, and a stamp into the AgentSession record.
  5. Latches until re-armed. Re-arming is always a deliberate operator action; the latch is never cleared automatically, and it deliberately has no MCP surface (an agent that could re-arm itself would defeat the human override). GUI host: the ⛔ Re-arm (Emergency Stop) menu item (visible only while stopped). Headless --mcp: a modal re-arm prompt (EmergencyStopRearmDialog) opens the moment the stop engages; Re-arm resumes, Leave stopped (also Enter/Esc/close; the fail-safe default) keeps the latch, and pressing the chord again reopens the prompt (EmergencyStop.Retriggered). The prompt is safe as a re-arm surface because the latch refuses every tool call while it is up, so only physical input can reach its buttons.

Step 1 (the latch) is the only work performed inside the low-level hook callback; steps 2–4 run immediately after on a background thread. A slow log or serial write can therefore never stall the WH_KEYBOARD_LL callback past LowLevelHooksTimeout, which would make Windows silently evict the hook; and the panic hotkey with it. Nothing actuates in the gap: the latch is already set, so every tool call is refused before the background steps even run.

Why it can’t be tripped or defeated by the agent

MCEC’s own agent actuation injects keystrokes and mouse input. Windows flags injected low-level events with LLKHF_INJECTED. The emergency stop reuses MCEC’s existing global WH_KEYBOARD_LL hook (HookManager) and reacts to physical input only; injected key events never arm a modifier and never trigger. This is what makes the hotkey a true human override rather than something the agent could press or hold to defeat.

Design / where it lives

Concern Location
Injected-flag surfaced on a physical-key event HookManager.Callbacks.csGlobalKeyEventArgs (KeyDownExt/KeyUpExt)
Chord state machine (pure, unit-tested) EmergencyStopDetector
Hotkey parsing EmergencyStopHotkey
Hook wiring + trigger/re-arm orchestration EmergencyStop
The latch AgentRuntime.EmergencyStopped
Actuation-gate refusal AgentServer.CallTool (emergency-stopped)
Cooperative drag abort MouseCommand.PerformDrag
Overlay banner CommandOverlayWindow
Re-arm affordance + host lifecycle (GUI) MainWindow (Start/Stop, SetUpEmergencyStopUi)
Re-arm affordance + host lifecycle (headless) HeadlessOperatorUi + EmergencyStopRearmDialog

The LL hook needs a message loop on its installing thread, so each host arms it where one pumps: the GUI host arms on the UI thread whenever EmergencyStopEnabled and the agent front door could be driving (McpServerEnabled || AgentCommandsEnabled); headless --mcp arms on HeadlessOperatorUi’s dedicated STA pump thread whenever EmergencyStopEnabled (the stdio transport is always a live front door, so there is no McpServerEnabled qualifier). That same headless pump thread hosts the command overlay, so a headless session narrates and shows the ⛔ STOPPED banner exactly like the GUI host.

Limitations


2. Isolated session provisioning

Isolated session provisioning gives an authorized agent a fresh, disposable copy of MCEC to drive instead of the operator’s installed instance. provision-session hands the agent a throwaway directory containing mcec.exe + dependencies and a co-located, agent-ready config (agent commands enabled only inside that copy). The agent runs from there and deletes it when done, so any enabled state lives only in the throwaway copy, “cleanup” is rm -rf <dir>, and a crashed or killed session leaves the real install untouched. Concurrent sessions each get their own directory and never contend.

The installed copy enforces its side of this: mcec.exe running from Program Files refuses mcp/--mcp and refuses to start the MCP/HTTP endpoint (Program.IsProgramFilesInstall), with an error pointing at provisioning or a manual writable copy. The operator’s install cannot be turned into an agent server even by editing its settings.

Flow

  1. Operator opts in once: the Allow agents to provision disposable instances checkbox on the Settings dialog’s Agent tab (AppSettings.AllowSessionProvisioning); the one thing that can’t be self-served, or the isolation is theater. This is the sole opt-in a non-technical operator performs to let an agent drive MCEC; it grants access only to a disposable copy, never this installed instance. The tab refuses to toggle it from inside a provisioned copy, so a driving agent can never widen its own permissions.
  2. Agent calls provision-session (optional mcpServer, commands). MCEC (SessionProvisioner):
    • creates %LOCALAPPDATA%\MCEC\sessions\<id>,
    • copies the binaries from the running exe’s dir (excluding the installed mcec.settings / mcec.commands / *.log),
    • writes a co-located mcec.settings (AgentCommandsEnabled=true, ActAsServer=false to avoid the firewall prompt, MCP bound to a free loopback port, AllowSessionProvisioning=false so it can’t re-provision, and McpAuthToken=<token>; see below) and a mcec.commands enabling the requested agent commands,
    • returns { sessionId, directory, exePath, mcpEndpoint?, token, launch, teardown }.
  3. Agent runs mcec.exe from directory and drives it. Over the session’s HTTP endpoint every request must carry Authorization: Bearer <token> (the instance’s own bearer-token gate enforces its configured McpAuthToken); the stdio transport is process-ownership-authenticated and needs no header.
  4. Agent calls end-session { sessionId, token } (after stopping the session’s exe) → the directory is deleted.

The session token

token is the session credential, doing real work on both surfaces:

The credential lives in the session directory itself (never in the installed config), so it survives installed-instance restarts and is retired the moment the directory is deleted.

Isolation approach: copy binaries

We copy the binaries into the session directory (rather than a --config-dir redirect of the installed exe). Because the copy runs from a non-Program Files location, Program.ConfigPath resolves to the session directory itself, so it reads its own co-located config with no launch flags. This is isolated by construction, and supersedes the earlier hand-rolled “copy the build and write a config” practice.

Teardown & reaping

Design / where it lives

Concern Location
Provision / teardown / reap SessionProvisioner
Handoff descriptor ProvisionedSession
MCP tools + authorization gate AgentServer (provision-session, end-session)
Operator opt-in + management UI Settings dialog Agent tab (AgentSettingsTab) → AppSettings.AllowSessionProvisioning
Session enumeration for the UI SessionProvisioner.ListSessions
Reap on launch Program.Main

Limitations