rightkit-control 0.1.11

Background native input, accessibility reads, and a WebDriver bridge for driving real desktop apps without stealing focus.
Documentation

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 bypass constrainFrameRect:toScreen: during setFrame:display:NO, and restores the original class immediately.
  • Windows uses SetWindowPos with no move, z-order change or activation.
  • Other platforms return a typed unsupported error.

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::EffectGate validates each effectful request, asks the host AdmissionHook for approval, executes it, and settles it. webdriver::Server::with_gate (click, send keys, screenshot), the tauri-plugin Control::gate, mac::Gated and the CLI all route effects through it.
  • Input lease (PTY-002). lease::InputLease acquire 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_lease makes the plugin's input methods require leaseEpoch and inputSequence and adds lease_* methods.
  • Durable lease state. InputLease::open with a LeaseStore. The default is in-memory. FileLeaseStore writes 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 an ExecutionIdentity and an ExecutionState (Running, Exited, Unknown). On restart the epoch is fenced forward, the lease is detached, Running comes back Unknown, and unacknowledged input comes back as Unknown input. It must be resolved with acknowledge_input(epoch, seq), reconcile or acquire_reconciling. A corrupt or foreign record fails closed rather than resetting.
  • Events. events::ControlEvent content-free lifecycle events through an EventSink.

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

  • FileLeaseStore assumes 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_reconciling panics if a durable store refuses the save. Durable callers should use try_acquire_reconciling.
  • The holder is identified by its epoch only. No holder label is stored.
  • Recovery runs in InputLease::open before an event sink is attached, so no recovery event is emitted. Inspect record() after opening.
  • LeaseError gained variants (ExecutionNotRunning, UnsentSequence, NonMonotonicAck, InvalidExecutionTransition, Store). Exhaustive external matches need a new arm.
  • The tauri-plugin lease methods do not yet expose durable state or acknowledge_input over the wire.
  • Breaking for direct mac callers: mac::click, type_text, key, ax_press, click_node, launch_hidden, capture_window, ax_make_key, key_without_raise, hover, drag and scroll moved to mac::Gated (or mac::unsafe_ungated). Reads (windows, main_window, frontmost_*, ax_dump, ax_find, ax_text, accessibility_trusted) are unchanged.
  • webdriver::macos::AxBackend is an adapter whose Backend methods act directly, and Server::backend is a public field. Only calls through Server::handle or serve are 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, and SetWindowPos for viewport sizing.
  • Other platforms: typed unsupported where 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/ with proof.sh (see above).
  • registration.patch at the crate root.

Version and changes

Published version: see INDEX.md. No CHANGELOG.md in this crate.