Agent Safety

Four related safety features for MCEC’s agent surface (MCEC’s computer use capability, in the same sense Claude, Codex, and similar agents use the term for seeing a screen and driving a keyboard and mouse). When MCEC is agent-driving the desktop, the target app has focus, not MCEC; so the operator needs (a) a controlled way to grant more capability mid-session, (b) assurance that a session cannot leave the installed copy in an unsafe state, (c) a way to instantly intervene when a run goes wrong, and (d) visible narration that MCEC is driving. Command-access consent, isolated session provisioning, the emergency stop, and the on-screen command overlay exist so the operator stays in control.

Getting started. How to opt in, click Provision new…, paste the handoff into your agent app, and tear down when done is in Agent Control — Quick start. This chapter covers the safety model behind that flow.


A provisioned session enables the standard agent tool set; everything else (launch, raw built-ins like chars: or shutdown, user-defined commands) starts disabled, and any tool or send_command that hits one is refused with error.code:command-disabled. The legitimate mid-session acquisition path is the request-command-access meta-tool: the agent names the command(s) (at most eight per request) and a one-line reason (required; truncated at 300 characters), and MCEC shows the operator a consent dialog (CommandAccessConsentDialog) on their desktop; GUI host on the main window’s thread, headless --mcp on HeadlessOperatorUi’s pump thread. The agent’s call blocks for the answer (bounded by a two-minute prompt timeout). Three explicit choices:

  1. Allow these commands — enables exactly the requested names (in-memory, this instance only).
  2. Allow these + any later requests — also arms a standing grant for this instance: future requests auto-approve without a prompt (the agent still calls request-command-access per command, and every grant is audit-logged and narrated on the overlay).
  3. Deny — sticky for this instance: re-requesting a denied command returns consent-denied without a prompt, so an agent cannot nag the operator into approval.

Deny is the default button; Esc and the close box count as deny (consent-denied). If the operator does not answer within two minutes, the prompt closes as a timeout (consent-timeout): that is not a sticky deny — the operator simply did not decide — so the agent may ask again later if the operator says to. An emergency stop engaging while the prompt is open dismisses it without recording a deny; the request-command-access call returns emergency-stopped (the session is halted, not refused permission), and the command stays grantable after re-arm.

Over the HTTP floor, request-command-access requires AgentCommandsEnabled=true (like every other agent tool). Over local stdio (mcec.exe --mcp) it is available alongside the documented send_command pass-through.

Why the agent can’t answer its own prompt

The agent being asked can synthesize input and drive UIA on the same desktop, so the dialog is protected by three layers:

  1. The call holds the input gate (AgentRuntime.InputGate) for the prompt’s whole lifetime, so no queued or synthesized keyboard/mouse input can land while it is up.
  2. Dispatch freezes everything except the flagged observation tools (ToolDescriptor.ServedDuringConsent; a new tool defaults to frozen) with consent-pending while a prompt is open. The freeze runs before the meta-tool branches, so it covers invoke (UIA-pattern actuation that deliberately does not take the input gate) and provision-session (which could otherwise mint a second, unfrozen instance to drive the dialog with).
  3. The dialog registers itself with WindowResolver as never-a-target (the overlay’s mechanism), so in-process targeting can never resolve it.

Residual risk: a second MCEC instance driven by the same agent shares none of these; the audit log and overlay narration are the mitigations there.

Why grants are in-memory only

A grant flips the command’s Enabled flag on the loaded table of the running instance; nothing is written to mcec.commands or mcec.settings, and CommandInvoker.Save serializes consent-granted commands as disabled, so even a later Commands-window “Save changes” cannot quietly persist an agent’s grant. A leaked session directory therefore never carries permissions beyond its provisioned defaults, a respawned instance resets and must ask again, and the co-located files remain a faithful record of what provisioning granted (the audit log holds what consent granted). Consent is deliberately MCEC’s own out-of-band dialog, never MCP elicitation, which would route the question through the agent’s own client — the party being constrained.

Design / where it lives

Concern Location
Consent state (single-flight, standing grant, sticky denies) AgentConsent
The consent dialog CommandAccessConsentDialog
Tool dispatch, gates, and the consent-pending freeze AgentToolExecutor (request-command-access, ConsentPendingRefusal)
GUI prompt channel MainWindow.PromptCommandAccess
Headless prompt channel HeadlessOperatorUi.PromptCommandAccess

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. MCEC 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 deleting the directory, 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 never serves the full agent surface. It refuses to start the MCP/HTTP endpoint (AgentServer.StartHttpProgram.IsProgramFilesInstall), and over --mcp it serves only the provisioning bootstrap (see below). The operator’s install cannot be turned into an observation/actuation agent server even by editing its settings.

Bootstrapping: how a fresh install gets its first instance

There is a chicken-and-egg to close. provision-session is itself an MCP tool, so an agent must reach some MCEC MCP server to call it; but a fresh install’s own copy refuses to serve agents, and a new user has no other copy yet. Two first hops resolve it:

Either way the agent gets back a non-installed disposable copy that serves the full tool surface, and drives that.

What provisioning creates

When a session is created (via Provision new… or provision-session over the bootstrap surface), MCEC (SessionProvisioner):

The operator opt-in (AppSettings.AllowSessionProvisioning) is the one thing that can’t be self-served, or the isolation is theater. The Agent tab refuses to toggle it from inside a provisioned copy, so a driving agent can never widen its own provisioning permission.

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.

Teardown & reaping

Design / where it lives

Concern Location
Provision / teardown / reap SessionProvisioner
Handoff descriptor ProvisionedSession
MCP tools + authorization gate AgentServer (provision-session, end-session)
Bootstrap-only MCP surface Program.ProvisioningBootstrapOnly, JsonRpcDispatcher (restricted tools/list + BootstrapInstructions), AgentToolExecutor.CallTool (bootstrap-only)
Operator opt-in + management UI Settings dialog Agent tab (AgentSettingsTab) → AppSettings.AllowSessionProvisioning
Operator “Provision new…” handoff ProvisionedInstanceDialog
Session enumeration for the UI SessionProvisioner.ListSessions
Reap on launch Program.Main

Limitations


3. 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 session. AgentRuntime.EmergencyStopped is set; AgentToolExecutor.CallTool refuses every tool call (actuation, observation, raw send_command, and request-command-access) 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 (GUI host), 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
Tool-call refusal AgentToolExecutor.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 when enabled, so a headless session narrates and shows the ⛔ STOPPED banner exactly like the GUI host.

Limitations


4. On-screen command overlay

Goal

Anyone watching the screen should see, at a glance, that MCEC is driving the machine and what it is doing. The overlay is the operator-facing narration layer: it does not gate or sandbox anything, but it makes agent activity impossible to miss while a session is live.

What it shows

When enabled, a borderless, top-most, click-through layered window (CommandOverlayWindow) spans the primary display:

The overlay registers its handle with WindowResolver as never-a-target, so an agent’s query/find/ invoke/click can never resolve or drive the overlay itself (the same mechanism protects the consent dialog). It never takes focus or input (WS_EX_NOACTIVATE + WS_EX_TRANSPARENT).

Agent commands only. The overlay subscribes to the agent execution seam (AgentToolExecutor and emergency-stop events). Classic TCP/serial remote-control commands do not publish to it; a socket-server-only instance never shows the overlay.

When it appears

The overlay is on by default (CommandOverlayEnabled=true) but only when an agent front door is open:

Turn it off in File ▸ Settings (or CommandOverlayEnabled=false in mcec.settings) if you accept running without on-screen narration; AGENT-AUDIT: log lines still record every agent action.

Design / where it lives

Concern Location
Overlay window + feed rendering CommandOverlayWindow
Event bus CommandEventHub
Tersified labels CommandTersifier
Publish from agent tools AgentToolExecutor.PublishOverlayEvent
GUI host lifecycle MainWindow.ShouldShowCommandOverlay / EnsureCommandOverlay
Headless host lifecycle HeadlessOperatorUi
Settings AppSettings.CommandOverlayEnabled, AppSettings.CommandOverlayPosition