rightkit-control
Shared native desktop control for Right Suite applications: drive a real desktop app like a person would, in the background, without taking focus. It provides background native input, accessibility reads, a WebDriver bridge, an embedded Tauri control server (behind a feature), and a rightkit-control CLI. Every effectful request passes one admission gate, and input is fenced by an epoch-based lease.
Catalog capability: desktop-control. Published to crates.io (see INDEX.md).
What it owns
| Module | Owns |
|---|---|
admission |
EffectGate, AdmissionHook (trait or closure), AllowAll, Effect and Settlement. Every effect is validated, admitted, executed and settled as ok, denied, failed or cancelled. |
lease, lease_store |
InputLease and ControlSession: input carries (epoch, sequence). Optional durable state through FileLeaseStore. |
events |
ControlEvent lifecycle events (start, stop, crash, denied, lease acquired, fenced, released, rejected) via an EventSink. Content-free. |
mac |
macOS primitives. Effectful ones are reachable only through mac::Gated; mac::unsafe_ungated is the explicit opt-in. |
webdriver |
webdriver::Server with with_gate, plus webdriver::macos::AxBackend. |
embedded |
The Tauri control server behind the tauri-plugin feature. |
cli |
rightkit-control command parsing and the Policy file. |
keys, json |
Key chord mapping and small JSON helpers. |
When to use it
- Use it to script a real desktop app or a WebView app in the background, with input that does not move the user's cursor or activate the app.
- Use it when effects need a host admission hook, input needs a lease, or events must go to a journal.
Do not use it for a web page that a headless browser can drive (see rightkit-browser). Do not use it for general cross-app automation with a four-tier ladder (see rightkit-automation).
Cargo features
| Feature | Default | Pulls in | Enables |
|---|---|---|---|
wv2-0_38 |
yes | windows 0.61, webview2-com 0.38 (Windows only) |
WebView2 types for Tauri 2.11 (wry 0.55). |
wv2-0_39 |
no | windows 0.62, webview2-com 0.39 (Windows only) |
WebView2 types for Tauri 2.12 and later. |
tauri-plugin |
no | tauri 2, rightkit-service, serde_json |
The embedded Tauri control server (embedded module). |
For Tauri 2.12 or later, use default-features = false with features = ["tauri-plugin", "wv2-0_39"]. Exactly one WebView2 generation should be enabled.
Usage
CLI syntax from the crate docs:
rightkit-control [--policy FILE] [--events] <command> ...
<command> is one of launch click hover drag scroll click-text press keywin keyraw type key shot, and serve handles click, send keys and screenshot. Read-only commands are not effects.
Rust integration goes through the gate. The default gate is allow-all, so existing callers keep their behaviour. A host supplies an AdmissionHook (trait or closure) to refuse effects. A denial, or a panicking hook, happens before any side effect.
Hidden macOS pointer input
Embedded Tauri targets receive native NSEvent down, drag and up calls on the WKWebView responder, on AppKit's main thread. This bypasses window first-mouse handling without making a window key, showing it, or activating its app. WebKit reads +[NSEvent pressedMouseButtons] to build Pointer Events, so the embedded path supplies a thread-local button mask during dispatch. A second scoped hook bypasses WKWebView's asynchronous pointer text-input interception. Both Objective-C hooks forward to their original implementations outside that dispatch. No DOM input events or global HID events are synthesized.
pointer {phase:"down"|"drag"|"up"|"move",x,y,button?,modifiers?} supports scrubs split across requests. click, move and composite drag share native delivery; x and y are viewport CSS pixels. Pointer calls keep effect admission, allowlists and optional input-lease fencing. rightkit-qa::Control::pointer uses this service for hidden launches. Non-embedded callers use the per-pid CGEvent fallback, which cannot guarantee first-mouse delivery to inactive views.
examples/control-test-app/proof.sh proves trusted Pointer Events, capture, scrub value changes, modifier clicks, hidden and non-key readback, and foreground stability throughout the run. TARGET names the workspace managed debug directory. APP_TARGET names the standalone fixture's managed debug directory.
Native viewport sizing
set_viewport {width:1440,height:1000} accepts integer CSS dimensions in 1..=16384 and returns the measured innerWidth and innerHeight. It passes effect admission and never activates, orders or makes windows key.
- macOS computes
frameRectForContentRect:on AppKit's main thread. It temporarily subclasses only the target NSWindow to bypassconstrainFrameRect:toScreen:duringsetFrame:display:NO, and restores the original class immediately. - Windows uses
SetWindowPoswith no move, z-order change or activation. - Other platforms return a typed
unsupportederror.
On macOS, the hidden WKWebView is resized explicitly and native layout completes before page dimensions are polled outside AppKit's main-thread block. The readback retries zero or stale dimensions and transient JS failures for up to 2 s, including callback timeouts. A failure reports both the requested and the last measured dimensions.
rightkit-qa::Control::set_viewport(width, height) returns the measured dimensions. The fixture proof checks hidden 1440×1000 CSS content, scale-aware PNG dimensions and foreground stability.
Control-plane guarantees
- Admission (EFF-001).
admission::EffectGatevalidates each effectful request, asks the hostAdmissionHookfor approval, executes it, and settles it.webdriver::Server::with_gate(click, send keys, screenshot), thetauri-pluginControl::gate,mac::Gatedand the CLI all route effects through it. - Input lease (PTY-002).
lease::InputLeaseacquire bumps the epoch and fences the previous holder. Stale epochs are rejected. Duplicate acknowledged input is deduplicated without re-execution. Gaps are rejected. Ambiguous (unacknowledged) input must be explicitly reconciled before new input.Control::input_leasemakes the plugin's input methods requireleaseEpochandinputSequenceand addslease_*methods. - Durable lease state.
InputLease::openwith aLeaseStore. The default is in-memory.FileLeaseStorewrites atomically (temp file, fsync, rename, directory fsync, checksum). An epoch is granted only once durable, and an input sequence is reserved durably before its executor runs. Each lease has anExecutionIdentityand anExecutionState(Running,Exited,Unknown). On restart the epoch is fenced forward, the lease is detached,Runningcomes backUnknown, and unacknowledged input comes back asUnknowninput. It must be resolved withacknowledge_input(epoch, seq),reconcileoracquire_reconciling. A corrupt or foreign record fails closed rather than resetting. - Events.
events::ControlEventcontent-free lifecycle events through anEventSink.
CLI policy
Without a policy, the gate is allow-all and output is unchanged. With --policy FILE or RIGHTKIT_CONTROL_POLICY, the gate applies cli::Policy: default allow|deny, then first-match allow|deny <kind|*> [method|*] lines. An unreadable or invalid policy exits 2. A denial exits 3 and does nothing. --events or RIGHTKIT_CONTROL_EVENTS=stderr prints content-free lifecycle events to stderr.
Known gaps
FileLeaseStoreassumes one owning process per file. It has no cross-process lock.- If the post-execute acknowledgement save fails, the durable record stays conservative: that input recovers as
Unknown.InputLease::store_degraded()reports it. acquire_reconcilingpanics if a durable store refuses the save. Durable callers should usetry_acquire_reconciling.- The holder is identified by its epoch only. No holder label is stored.
- Recovery runs in
InputLease::openbefore an event sink is attached, so no recovery event is emitted. Inspectrecord()after opening. LeaseErrorgained variants (ExecutionNotRunning,UnsentSequence,NonMonotonicAck,InvalidExecutionTransition,Store). Exhaustive external matches need a new arm.- The
tauri-pluginlease methods do not yet expose durable state oracknowledge_inputover the wire. - Breaking for direct
maccallers:mac::click,type_text,key,ax_press,click_node,launch_hidden,capture_window,ax_make_key,key_without_raise,hover,dragandscrollmoved tomac::Gated(ormac::unsafe_ungated). Reads (windows,main_window,frontmost_*,ax_dump,ax_find,ax_text,accessibility_trusted) are unchanged. webdriver::macos::AxBackendis an adapter whoseBackendmethods act directly, andServer::backendis a public field. Only calls throughServer::handleorserveare admitted.
Platform support
- macOS: accessibility reads, per-pid events (
CGEventPostToPid, never the global HID tap), the embedded WKWebView path, and captures. - Windows: WebView2 through the
wv2-*features, andSetWindowPosfor viewport sizing. - Other platforms: typed
unsupportedwhere the embedded path has no implementation.
Tests and examples
tests/control_plane.rs,tests/durable_lease.rs,tests/gated_paths.rs,tests/serve_auth.rs.examples/control-test-app/withproof.sh(see above).registration.patchat the crate root.
Version and changes
Published version: see INDEX.md. No CHANGELOG.md in this crate.