rightkit-tauri-shell
Shared Tauri desktop shell for Right Suite applications. It owns hardened webviews (HardeningScript, navigation and popup policy), window state, the native menu and tray, single-instance arbitration, deep links, autostart, rich clipboard, notifications, a watchdog, presence, and optional engine sidecar wiring. Its entry point is Shell::builder(app_id).build().plugin(), with lower-level modules public.
Use it in a Right Suite Tauri app's native host, so every app gets the same shell behavior.
Do not use it outside a Tauri 2 application, or as a replacement for an app's own invoke handlers and product logic. Choose one plugin entry point per app.
Features
| Feature | Default | Pulls in | Effect |
|---|---|---|---|
engine |
off | rightkit-job-queue (optional; rightkit-child-process is an unconditional dependency) |
Engine sidecar wiring over rightkit-job-queue EngineClient. Opt in; it pulls rightkit-job-queue (db, time, uuid). |
wv2-0_38 |
on | windows 0.61, webview2-com 0.38 |
Windows types for Tauri 2.11 (the default). Must match the app's Tauri. |
wv2-0_39 |
off | windows 0.62, webview2-com 0.39 |
Windows types for Tauri 2.12. With default-features = false, enable wv2-0_39 (plus engine if used). Enabling both selects 0.39. |
content-devtools |
off | tauri/devtools |
Required by DevToolsPolicy::ContentOnly. Only an app on the right-release allow-list (today ViewRight) may ship it; see "DevTools policy". Never a default feature. |
dev-overrides |
off | none | Development and QA builds only: NavPolicy::default() treats loopback (localhost, 127.0.0.1, [::1]) http(s) URLs as internal. Not implied by a build profile (installed dev builds with debug assertions get the release default, allow_loopback: false); an app sets it from an optional QA/dev feature. Never a default feature. |
dev-inspect |
off | tauri/devtools |
Development builds only: every webview is inspectable and the DevTools guards stand down; see "DevTools policy / Development builds". A build with it can never be released. Never a default feature, and not needed by ContentOnly. |
Mismatched wv2 features put two copies of windows or webview2-com beside Tauri's, and cast() then fails. With default-features = false, enable exactly the feature that matches the app's Tauri version.
Platform support
- macOS: native notifications and presence through
objc2and related crates;cfg(target_os = "macos")insrc/(for examplenotify.rs,presence.rs,frameless.rs). - Windows: WebView2 types through the
wv2features; native toasts throughtauri-winrt-notification;cfg(windows)insrc/. - Linux: this README does not describe Linux-specific behavior. Open-URL delivery on Linux depends on the
.desktopMIME entry (see Standalone open-URL hook).
Published version: see INDEX.md at the repository root. See CHANGELOG.md; the version sections below record additions by version.
Example
Illustrative; not compiled as a doctest. The compiled references are tests/policy.rs (policy decisions) and the example-app/ Tauri project in this crate.
Windows
- Generation:
src/lib.rsre-exports onewindowsandwebview2-compair ascrate::winapi.wv2-0_38selects windows 0.61 and webview2-com 0.38 (default).wv2-0_39selects 0.62 and 0.39. Enabling both picks 0.39. With neither, the crate fails withcompile_error!. Tauri 2.12 apps usedefault-features = false, features = ["wv2-0_39"](add"engine"for the sidecar). - The Cargo.toml comment ties 0.38 to Tauri 2.11 and 0.39 to Tauri 2.12. The Tauri lock versions are not checked here: the example app's
Cargo.lockis stale and records rightkit-tauri-shell 0.2.0 (example-app/Cargo.lock:2692; the crate is 0.3.0). - WebView2 hardening (
src/harden.rs,windows_impl): sets status bar, zoom control, pinch zoom, swipe navigation and browser accelerator keys (F5, Ctrl+R, Alt+Left, F12). It registersContextMenuRequestedthroughICoreWebView2_11. That handler suppresses the menu unless the target is editable andallow_editable_menuis set. - Idle (
src/idle.rs):ICoreWebView2_3::TrySuspendandSetIsVisibleon the controller. - Clipboard (
src/clipboard.rs):OpenClipboardwith 10 retries at 20 ms. ReadsCF_UNICODETEXT,HTML Format,PNG,CF_HDROP(file lists throughDragQueryFileW) andCF_DIB. A write owns the clipboard through a message-only window, becauseSetClipboardDatafails afterEmptyClipboardwith a NULL owner.ClipboardOptions::image_preferencechoosesPNGfirst (default) orCF_DIBfirst for the typed image. - Notifications: WinRT toasts through
tauri-winrt-notification0.8, as described below. - Registry and shell: autostart reads and writes
HKCU\...\CurrentVersion\Runthroughreg.exe. Scheme registration writes underHKCU\Software\Classesthroughreg.exe. Both useCREATE_NO_WINDOW.openerlaunchesrundll32.exe url.dll,FileProtocolHandler. - Single instance uses the rightkit-daemon named pipe (
src/instance.rs). Broker entropy comes fromSystemFunction036in advapi32 (src/broker.rs). - Dependencies:
windows_061/windows_062andwebview2_com_038/webview2_com_039are optional. They enable Win32 featuresWin32_Foundation,Win32_System_Com,Win32_System_DataExchange,Win32_System_Memory,Win32_System_Ole,Win32_UI_ShellandWin32_UI_WindowsAndMessaging. Consumers select only awv2-*feature. - Caveat:
harden.rs,clipboard.rsandidle.rsarecfg(windows)code and are not compiled or exercised on the macOS host (src/idle.rs:9). The clipboard backend has not been run against a live Windows clipboard, so verify copy and paste on a Windows machine before relying on it.
DevTools policy
The owner's fixed design, verbatim, never to be changed: every Right app has absolutely NO access to DevTools: blocked at each and every level that is possible, for the app, its shell and its own UI. The single exception is ViewRight: it opens HTML documents, and DevTools must be available INSIDE that opened HTML content only (the content frame), never for ViewRight's own UI or shell.
Source: src/devtools.rs (OWNER_DECISION). Written, unverified: nothing here was compiled or run; the macOS and Windows calls are the uncertain part.
API
DevToolsPolicy::Denied(the default) andDevToolsPolicy::ContentOnly(exactly two values).Shell::builder(id).devtools_policy(..).ContentWebview::new(label, window_label).capability(json)...app_acl_manifest(true)andShell::builder(id).content_webview(..)register a webview that renders opened content. Registration is refused (typedDevToolsError, startup fails, loud stderr line) when: the policy isDenied; thecontent-devtoolsfeature is not compiled; the label is the main webview (never allowed); a supplied capability document applies to the content webview by webview label or by the label of its parent window and grants permissions; a capability document cannot be parsed; the app did not attest an application ACL manifest.devtools::guard_process_start()runs when the plugin is built and may be called first inmain.devtools::open_content_devtools(app, label)is the only path to an open inspector and refuses anything but a registered content webview.- The old
Devtoolsenum andHardening::devtoolsare removed: there is no "on" and no "debug builds" any more. - The policy is not a
Module: opting out ofModule::JsHardeningdoes not opt out of it.
What each layer blocks
| Layer | macOS (WKWebView) | Windows (WebView2) |
|---|---|---|
| Per webview, native | isInspectable = false (macOS 13.3+, guarded by respondsToSelector) and the private developerExtrasEnabled preference set to false explicitly, on every webview the shell sees, including debug builds. This removes Safari Develop attach, "Inspect Element" and WebKit's Cmd+Opt+I handler. close_devtools runs first where Tauri has one. |
AreDevToolsEnabled = false, read back; when it cannot be set or still reads on, the webview is closed and DevToolsError::NativePolicyNotApplied is reported (apply_native returns it when Tauri ran the native step at once, otherwise it is printed). One AcceleratorKeyPressed handler per webview stops exactly the keys the active rules name: F12, Ctrl+Shift+I/J/C and Ctrl+U in a denied webview; F5, Ctrl+R and Ctrl+Shift+R only with the hardening's block_reload; Alt+Left/Right and the back/forward keys only with block_history_nav; Ctrl with plus, minus or zero only with block_zoom. Browser keys are never turned off as a whole, so Ctrl+P and Ctrl+F always pass. A ContextMenuRequested handler removes every menu item whose name contains "inspect" from every menu, including the editable-field menu the hardening keeps. Each webview has one handler of each kind, however often it becomes ready. |
| Page script | F12, Ctrl+Shift+I/J/C, Cmd+Opt+I/J/C/U, Ctrl+Alt+I/J/C/U cancelled in every frame of every non-content webview (matched by KeyboardEvent.code, so Option-letter dead keys do not slip through). |
Same. |
| Process start | Refuses a launch argument that enables remote debugging. | Same, plus: removes the WebView2 additional-browser-arguments and script-debugger-pipe environment variables from the process; refuses to start when the AdditionalBrowserArguments machine or user policy (HKLM in the 64-bit and the 32-bit registry view, HKCU) injects a debugging switch, or when one of those keys exists and cannot be read. The keys are read through the registry API; no program is started for it. Only a key that does not exist counts as "no policy". |
| After creation | none | A fresh DevToolsActivePort file in the app's local or roaming data folder (or its EBWebView) means a debugging port is open: the process logs and exits 1. The folders looked in are the app's local and roaming data, the folder a UserDataFolder policy names for this program, the folder the WebView2 user-data environment variable names, and any folder the app registered with devtools::register_user_data_dir (needed when a webview is given its own data_directory). Checked when a webview becomes ready and once more when the first page has finished loading. |
What cannot be blocked
- Windows machine policy. WebView2 appends policy and environment arguments to whatever the app passes (Microsoft's WebView2 documentation; not fetched or checked in this repository), so an explicit argument string cannot override them. The environment variable is process-local and is removed. The registry policy is not ours: the shell detects it at startup and refuses to run. An administrator can still write the key after the check; the port-file tripwire is the second line. Elevated host apps ignore the environment variable (Microsoft's WebView2 documentation; not fetched or checked in this repository), so this matters most for non-elevated apps.
- A debugger attached to the process (lldb/Visual Studio,
get-task-allow, Time Travel Debugging) and UI automation / accessibility are outside "DevTools" and outside this crate. The hardened runtime and signing checks inright-releaseare the control for the macOS entitlement. - macOS before 13.3 has no
isInspectable; only thedeveloperExtrasEnabledpreference and the absence ofget-task-allowapply. - The user's own
WebKitDeveloperExtrasdefault is overridden by the explicit per-webview preference; this ordering is unverified. - Linux is not covered by native settings (Tauri on Linux is not a supported Right target).
ContentOnly
ContentOnly makes only registered content webviews inspectable: isInspectable = true + developerExtrasEnabled = true (macOS); AreDevToolsEnabled = true with a context menu trimmed to the Inspect item, accelerator keys still off, so the inspector is opened from that item or from open_content_devtools (Windows). The shell's DOM, Console and Sources are never reachable because the shell is a different WKWebView / CoreWebView2 whose own setting stays off.
Iframes: inspection of an iframe alone cannot be granted. On both platforms the inspector attaches to a whole webview, so a page whose content is an iframe in the app UI webview would make the UI inspectable. Content must live in its own webview. ViewRight renders HTML previews as iframes today and does not meet this rule; see docs/plans/2026-10-11-viewright-content-devtools-design.md.
Adoption
- Every app: nothing to call. Do not enable Tauri's
devtoolsfeature, do not useDevtools::*(removed), do not create webviews with.devtools(true). - An app that created webviews itself: they pass through
on_webview_readylike any other and are denied. - ViewRight only: enable
content-devtools,devtools_policy(ContentOnly), register each content webview, declare an application ACL manifest inbuild.rs, scope its capabilities withwebviews(notwindows) and set"devtools": "content-only"in itsright-releaseconfig. right-releaserefusesdevtoolsin a release or update build for any app that is not allow-listed (packages/release/devtools-policy.json).
Development builds
The owner's rule for shipped apps is unchanged: no DevTools, ViewRight's content frame excepted. Developers and the QA tooling still need to inspect a development build, so exactly one switch exists, the cargo feature dev-inspect (off by default, never reached from default, and not needed by ContentOnly; a content-devtools release build never compiles it).
- The pipe form of remote debugging leaves no port file. Started with the remote-debugging pipe switch, a browser takes its debugger over inherited handles: it opens no port and writes no
DevToolsActivePort, so the tripwire above cannot see it. The switch is refused where it can be seen (the process's own arguments, theAdditionalBrowserArgumentspolicy, the scrubbed environment variables). A parent process that starts the browser itself with that switch is outside what this crate can detect. BrowserExecutableFolderpolicy (Windows). A machine or user policy that makes WebView2 start a browser from another folder for this program is refused at start (DevToolsError::BrowserRuntimeRedirected): that browser may be built to do anything. Matched value names:*and this program's executable file name, compared without regard to case; every other name belongs to another program and is ignored. The environment variable of the same purpose,WEBVIEW2_BROWSER_EXECUTABLE_FOLDER, is refused the same way when it is set in the process environment at start.- WebView2 environment variables (Windows). Inspected at start: the additional-browser-arguments variable and the script-debugger-pipe variable (both removed from the process),
WEBVIEW2_BROWSER_EXECUTABLE_FOLDER(refused when set),WEBVIEW2_USER_DATA_FOLDER(left in place; the folder it names is watched by the port-file tripwire). Knowingly not inspected:WEBVIEW2_RELEASE_CHANNEL_PREFERENCEand the other channel-selection variables, which choose among Microsoft's own runtimes. - Only under
dev-inspect: the policy for the app's webviews becomes "developer inspection allowed". Every webview, the app UI included, is inspectable (WebviewDecision::AllowDev; macOSisInspectableanddeveloperExtrasEnabledtrue, WindowsAreDevToolsEnabledtrue, menus and accelerator keys left as the hardening set them);guard_process_startstops scrubbingWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, stops refusing a debugging launch argument or registry policy, and theDevToolsActivePorttripwire does not run, so a remote-debugging port (the wdio WebDriver driver ofqa-nativebuilds) keeps working. The build embeds the markerRIGHTKIT-SHELL-DEV-INSPECT-COMPILED(DEV_INSPECT_MARKER, a#[used]static read from startup code, the techniqueRIGHTKIT-QA-CONTROL-SERVER-PRESENTuses) and prints one stderr line at startup beginningRIGHTKIT SHELL DEVELOPER INSPECTION ACTIVE. - Without the feature nothing changes:
Denied/ContentOnlyexactly as above, in every build profile (debug assertions do not matter, because installed dev builds are release-profile builds). - Selecting it: an app adds an optional feature that its default features never reach,
dev-native = ["rightkit-tauri-shell/dev-inspect"](besideqa-native), and builds its development lane with--features dev-native. - Such a build can never be released, three layers: (1)
right-releaserefusesdev-inspect(anddev-native) in a release or update build command, package script, or Cargo manifest, directly, through another feature, throughdefault, or via--all-features(qa-build-features.mjs); (2) the binary scan refuses the marker in every release and update artifact, including inside MSIX/ZIP containers, and nodevtools: "content-only"allowance lifts it (qa-control-scan.mjs, ruleshell-dev-inspect); (3) therightkit-qanative QA guard rejectsdev-inspect/dev-nativereachable from default features, a non-optionalrightkit-tauri-shellthat lists it, and release-named package scripts that enable it. The development lane (right-release dev) never runs layers 1 and 2.
Hidden hosts: never_show, reopen opt-out, idle options
Shell::builder(..).never_show(true)and.never_show_env("RIGHTKIT_QA_NATIVE_REAL"): the shell shows, unminimises and focuses nothing under any event, and macOS runs Prohibited from plugin setup. Every reveal path goes throughlifecycle::reveal_mode; the host's own direct window calls are outside it..reveal_on_reopen(false)(orShellPlugin::builder().without(Module::RevealOnReopen)): Dock reopen and opened URLs no longer reveal; links still deliver.never_show_env(name)counts only1,true,yes,on(case-insensitive, exact); other values are off.never_show_env_value(name, "token")matches that exact value only.- Idle ordering (Windows): suspend =
SetIsVisible(false)thenTrySuspend, skipped if the hide failed; resume =ResumethenSetIsVisible(true), skipped if the resume failed; failures reachwebview_idle_on_errorand are retried on the next resume; resume does nothing if this shell did not hide or suspend.webview_idle_on_transition(|t| ..)logs real suspends and resumes with elapsed time;idle::note_minimized_aftertakes a per-minimise delay. .webview_idle_options(IdleOptions { start_on_minimize: true, controller_hide: ControllerHide::OnTimer, events: IdleEvents::NativeSuspendOnly, ..Default::default() }). Defaults matchwebview_idle(30 s).IdleOptions::default().suspend_after_with(|| Some(read_delay()))reads the suspend delay at each hide (Noneor a panic uses the fixedsuspend_after).IdleOptions::default().on_minimise(true)(IdleTrigger::Minimised): idle only when the window is minimised (Windows:SetIsVisible(false)thenTrySuspend, restore isResumethenSetIsVisible(true)); a tray hide does not start it. For a single-web-view app; macOS and Linux get the event only.lifecycle_onlyleavesShowOnReadyand the cache repair off unless the host callsshow_on_ready(true)/repair_cache(true).
Also (0.3.0, host-decided lifecycle)
- Quit drain:
quit_drain(true)/quit_drain_bound(d); the renderer (@rightkit/tauri-shell0.1.1) answersrightkit-shell://quit-requestedand callsrk_shell_finish_quit. Default bound 15 s, max 120 s; off by default. close_behavior(CloseBehavior::HideToBackground),on_visibility,qa_instance_exempt,second_instance_reveal,show_on_ready(explicit beatsRIGHTKIT_QA_HIDDEN),internal_scheme.Shell::lifecycle_plugin(): the lifecycle without hardening, navigation policy, presence, watchdog or window state.WindowStateStorestandalone withrestore_with;AutostartOptions::{startup_wm_class, config_home, macos_unsupported}.- macOS login item through
SMAppService.mainApp(macOS 13+):AutostartOptions::macos_backend(MacosBackend::ServiceManagement)(defaultLaunchAgent, unchanged).rk_shell_autostartthen returnsloginItem(notRegistered | enabled | requiresApproval | notFound); onrequiresApproval(enabledis false) callrk_shell_open_login_items_settings/login_item::open_login_items_settings()to send the user to System Settings > Login Items. Needs a signed.appbundle; carries no launch arguments (detect the login launch yourself) and noLowPriorityIO(uselogin_item::set_background_io). An existing LaunchAgent plist is removed and replaced at startup; macOS 12 and older, an unbundled executable and a redirectedRIGHTKIT_SHELL_HOME/config_homekeep the LaunchAgent. Mac-only behaviour is verified on a Mac. NotificationRequest.bypassDoNotDisturb;rk_shell_capabilities.
Also (0.3.0)
Modular install and the broker as a library type
use ;
// Everything except presence and the watchdog:
let plugin = builder
.shell
.without
.without
.build;
// Only broker, stream, recovery, frameless and clipboard state:
let plugin = builder
.shell
.install_state_only;
An opted-out command family answers unsupported. PathBroker needs no plugin: create one
in your own state and call its grant and require_* methods from your own commands.
Site-root scheme
ShellBuilder::site_protocol("viewright-site", SitePolicy::default()) serves allowlisted
subresources beneath a site root. The app grants it for a document it already holds with
rk_shell_site_grant (which returns {token, root, baseUrl}) and ends it with
rk_shell_site_revoke. Denied names and the MIME allowlist are part of SitePolicy.
Typed clipboard, recovery, idle and single instance
rk_shell_clipboard_itemreturns{"kind": "files" | "image" | "text" | "none", ...}. Images are written to a granted temp file that is purged afterclipboard_purge_after(default one hour).RecoverySpec::documents().at_path(p)pins the snapshot file.RecoveryStore::migrate_legacydoes a one-time, validated migration from an older path.webview_idle_on_error(|failure| ..)reports failed main-webview suspend or resume calls.SingleInstance::claim(id, on_message)returnsClaimOutcome::{Primary, Forwarded, Failed}. TreatForwardedandFailedas exit paths.Failednever becomes a second primary.startup_cache::ensure_http_cache_dir(identifier)repairs the user cache directory for the app.ensure_http_cache_dir_with(identifier, || dirs::cache_dir())does the same under the root your Tauri resolves (dirs::cache_dir, or the path resolver once anAppHandleexists).- Private clipboard for tests:
clipboard::take_item_from_with(&Board, broker, temp_dir, policy, &options)istake_item_withfor an explicit board (take_item_withis itsBoard::Generalwrapper). WithBoard::Named(name)on macOS it reads only that private pasteboard and never touches the general clipboard. The AppleScript fallbacks (a file URL without a path, a PNG coerced from a TIFF) read the general clipboard, so they are off for a named board and nothing falls back to it: a missing type or an empty board givesClipboardItem::None, never the user's clipboard. Stage content withclipboard::write(&Board::Named(name), ..). Use a unique name (for example<app>-test-<pid>-<n>); theApple CFPasteboard ...names are the system boards (general, find, font, ruler, drag) and are refused byread,write,formatsandtake_item_from_with, as is an empty name. Windows has one clipboard and no private one: a named board is rejected there (read,write,formatsandtake_item_from_withreturn an error), so a test that needs a private board is macOS-only. Other platforms reject it too. - QA controls (non-default feature
qa-probes, development/QA builds only):clipboard::qa::paste_from_board(&broker, app_id, raw)reads a private named pasteboard as typed JSON, andclipboard::qa::stage_board(raw, prefix)stages a file URL and/or a generated PNG on a board whose name starts with the app's QA prefix; both refuseApple CFPasteboard ...names (clipboard::named_board_refusal) before touching a pasteboard. Register them asrightkit-qacontrol commands; seeexample-app/src/main.rsandexample-app/scenarios/. frameless::button_frames(&window)is a read-only macOS probe of the close, minimise and zoom buttons (frame, hidden flag, window visibility), read on the main thread; it returnsShellError::Unsupportedon other platforms. It is a QA probe: compiled only with the non-defaultqa-probesfeature in a debug-assertions build; a release build that enables the feature fails withcompile_error!.- Clipboard reads:
clipboard::readno longer copies bytes of unrecognised pasteboard types. Opt in withClipboardOptions { custom_max_bytes: Some(n), .. }throughclipboard::read_withorShellBuilder::clipboard_options. On macOS text also falls back to the other plain-text types (NSPasteboardTypeStringcoercion,NSStringPboardType, UTF-16 and generic plain text), aspbpastedoes.ClipboardItem::Nonestill serialises as"none"; an app whose front end expects"empty"maps it. StreamPolicy::default().observer(|event| ..)receivesStreamEvent { mime, kind }withStreamEventKind::{Resolved, Head, RangeServed, FullServed, RangeRejected, BytesRead}. Unset costs one branch per request; a panicking observer is caught and never reaches the response.frameless::TrafficLightsis a plain value (Default,Clone). An app without the shell plugin canapp.manage(TrafficLights::default()), callset_offset(relative to the macOS default) orset_inset(absolute: the close button's top-left cornerxpoints from the left andypoints from the top) from its own command and forward window events tohandle_window_event; noShellState, lifecycle hook or single-instance claim is needed. The shell's own app-menu quit item (macOS "Quit ", Windows/Linux "Exit ") routes to the ordered quit, so close-to-tray no longer swallows Cmd+Q.LeftClick::Showreveals the window on a tray left click and never hides it.- Navigation:
OAUTH_POPUP_ORIGINSnow includes the Atlassian and Salesforce sign-in origins.NavPolicy::decide_window_open_pinnedadmits a popup only from a given pinned opener origin;decide_window_openis unchanged and still returnsDenyfor a non-web scheme, andNavPolicy::protocol_consent_schemenames the scheme an app with a consent surface may confirm. The shell's own windows pass the opening window's URL to the popup decision.nav::strip_coop_headersandnav::popup_origins_from_jsonare pure helpers for the popup response hook and an app's popup-origin setting.
Relaunch for update, and the legacy plugins' names
SingleInstance::destroy() releases every claim this process holds through the shell (the
standalone claim and the plugin's primary), then runs each release registered with
SingleInstance::on_destroy, once. It is idempotent. It never touches a tauri-plugin-single-instance
claim by itself: ClaimOptions::legacy_identifier only probes for a legacy holder, and the plugin's
mutex, window, socket or D-Bus name belongs to the plugin's own state in the app. An app that still
registers the plugin makes one call release both:
let handle = app.clone;
on_destroy;
// relaunch-for-update, after spawning the helper:
destroy;
app.exit;
Without the hook, call tauri_plugin_single_instance::destroy(&app) and SingleInstance::destroy(),
both, before exiting. On Windows the plugin closes its mutex (which frees the name) but its
DestroyWindow only works on the thread that created the window, so the hidden window can outlive
the call until exit; the relaunch is not blocked, since arbitration is the mutex.
Exact legacy names (LegacySingleInstanceNames, LegacyAutostartNames), read from
tauri-plugin-single-instance 2.4.2 and 2.4.3, tauri-plugin-autostart 2.5.1, auto-launch 0.5.0.
HeardRight (identifier app.heardright.next, productName HeardRight):
| Item | Derivation | HeardRight |
|---|---|---|
| Windows mutex / window class / title | <identifier>-sim / -sic / -siw |
app.heardright.next-sim / -sic / -siw |
| macOS socket | /tmp/<identifier with . and - as _>_si.sock |
/tmp/app_heardright_next_si.sock |
| Linux D-Bus name (not probed) | <identifier>.SingleInstance |
app.heardright.next.SingleInstance |
| Windows Run value | HKCU\Software\Microsoft\Windows\CurrentVersion\Run, value <entry name> |
HeardRight |
| macOS LaunchAgent | label <entry name>, ~/Library/LaunchAgents/<entry name>.plist |
HeardRight, ~/Library/LaunchAgents/HeardRight.plist |
| Linux autostart | ~/.config/autostart/<entry name>.desktop |
HeardRight.desktop |
The entry name is the plugin builder's app_name, else PackageInfo::name, which is productName
when set and the Cargo package name otherwise. The names assume the plugins' semver feature is off
(HeardRight does not enable it). The plugin also writes a same-named value under
...\Explorer\StartupApproved\Run; the migration leaves it. HeardRight's lockfile pins
tauri-plugin-single-instance 2.4.5, whose source was not available to read; 2.4.2 to 2.4.3 changed
only the macOS listener (tokio), not the names.
Also (0.1.3)
Single-instance arbitration only
default
.plugin;
Add this plugin first. It uses only app_id from the builder & always enables
arbitration. It retains the primary service guard until exit, forwards argv/cwd
to the primary callback & exits a successful secondary launch. Unavailable
arbitration fails startup (the plugin setup returns an error): the app never runs as a second
primary. instance::arbitrate returns Arbitration::Unavailable for OS-level failures and
Arbitration::InvalidAppId for an unusable id; the caller must treat neither as "single instance off". The callback owns all second-launch behavior, including URL delivery & focus.
This entry point installs no hardening script, native hardening, navigation
policy, window-state restoration, presence loop or ShellState. Use your own
invoke handler; rightkit_tauri_shell::handler! requires the full plugin. Choose one
plugin entry point per app. The lower-level instance::arbitrate remains public.
Single-instance naming
The single-instance service is named <id>-shell (instance::arbitrate, the plugins) or
<id>-instance (SingleInstance::claim), after the id is normalised: ASCII-lowercased, then every
character outside [a-z0-9-] becomes -, one for one, nothing collapsed or trimmed. A dotted bundle
identifier works: app.mailright becomes app-mailright, Mail_Right becomes mail-right. An id that
is already [a-z0-9-] is unchanged, so running instances are still found across an upgrade. Distinct
ids can normalise to one name (a.b and a-b), which makes them the same app for this purpose.
rightkit-daemon allows 1 to 48 bytes of [a-z0-9-], so an empty id, or one longer than 42 bytes
(-shell) or 39 bytes (-instance), is refused with Arbitration::InvalidAppId(InvalidAppId) or
ClaimFailure::InvalidId, never with Unavailable or Host. instance::try_service_name(id) returns the
name or the typed error. ShellBuilder::build still panics on any id outside [a-z0-9-] and 40 bytes.
Full-plugin script scope
use ;
let plugin = builder
.hardening_script
.build
.plugin;
ShellBuilder::hardening_script accepts AllFrames (unchanged default),
TopFrameOnly or Off. Top-frame mode requests native main-frame injection &
guards the script body with window.top !== window.self, so document iframes
remain untouched even on WebView2, which injects scripts into subframes.
Off registers no DOM hardening script. Native webview hardening, navigation
policy & remaining full-shell behavior still apply in both modes.
Tests inspect generated plugin scripts for all three scopes & the isolated plugin, plus real service arbitration from a separate secondary process, argv/cwd forwarding & guard release.
Also (0.1.1)
Windows notifications
notify now posts native toasts on Windows (notify::platform::SUPPORTED is true), through tauri-winrt-notification, the WinRT ToastNotificationManager wrapper that tauri-plugin-notification uses via notify-rust. The public API (notify::post, status, NotificationRequest, sound throttling, Do-Not-Disturb suppression) is unchanged, so callers are cross-platform.
- Toasts are attributed to an AppUserModelID. An installed app owns one through its Start-menu shortcut; Tauri's NSIS and MSI installers set it to the bundle
identifier, and the shell usestauri.conf.json'sidentifierby default. Callnotify::set_app_id("com.example.app")before the first notification to override it. The installer or shortcut must register that same AUMID, or Windows drops the toast. - An unpackaged dev build has no AUMID, so toasts fall back to PowerShell's (
notify::FALLBACK_APP_ID) and show its name and icon. sound_nameacceptsDefault,IM,Mail,Reminder,SMS; any other name (for example the macOSGlass) plays the default toast sound.- Windows has no per-app permission prompt and this backend cannot read delivered toasts:
authorizationreportsauthorized,deliveredis empty, andclear_deliveredis a no-op.
Standalone open-URL hook
open_url delivers URLs the OS opens in the app, without the Shell plugin:
on_open_url;
let cold = current_urls; // arrived before any handler existed
- macOS: call
open_url::handle_run_event(&event)fromapp.run(|_, event| ...)(Apple Event URLs,RunEvent::Opened). - Windows/Linux:
open_url::init_single_instance(app_id, version)runsinstance::arbitrate, captures this launch's command-line URLs and delivers a second launch's argv to the first instance (exit onArbitration::Secondary). Without single instance, callopen_url::capture_launch_args()once at startup. on_open_urlhandlers receive URLs that arrive after registration, in registration order;current_urls()keeps the cold-start ones. The same URL withinDEDUP_WINDOW(2 s) is delivered once. Onlyscheme:restentries with a scheme of two or more characters count, so flags andC:\paths are ignored.- The full Shell plugin feeds the same hub.
- Scheme registration (
mailto:, custom schemes) stays with the app's installer or bundle:CFBundleURLTypeson macOS, a protocol entry (NSIS/MSI ordeep_link::register_scheme) on Windows, a.desktopMIME entry on Linux.
Second-instance callback, dependency ranges
Shell::builder(..).on_second_instance(|app, argv, cwd| ..) replaces the default second-launch behaviour (event plus reveal of the main window); the callback owns focus. Arbitration is unchanged, and URLs on the forwarded argv still reach open_url handlers and current_urls() before the callback runs. Without a callback nothing changes.
windows/webview2-com are feature-selected so they match the app's Tauri: wv2-0_38 (0.61/0.38, Tauri 2.11, default since 0.2.0, CHANGELOG.md) or wv2-0_39 (0.62/0.39, Tauri 2.12). A mismatch puts two copies beside Tauri's and cast() fails (E0599, found by MailRight). With default-features = false, add one of them, e.g. features = ["wv2-0_38"] (an earlier default, wv2-0_39, broke Tauri 2.11 apps; found by CodeRight).
Runtime budget
Generated from runtime-budget.json; edit that file, then run node packages/release/runtime-budgets.mjs sync <crate-dir>.
- Resident memory ceiling: 119.0 MiB.
- CPU ceiling (idle, steady state): 1%.
- Wake-ups ceiling: 10.2 per second.
- Disk operations ceiling: 1 operations per second.
- Required scale: 1 watchdogs or more.
- Required hosts: macOS only (Adrian 2026-10-14 local-build grant (.rightkit-local-build.json); Windows covered by the next CI run).
- Status exposure:
status()insrc/watchdog.rs; exposes resident memory, idle CPU, wake-ups, disk operations. - Measured: win32/x64 (13th Gen Intel(R) Core(TM) i9-13900H), rightkit-tauri-shell 0.3.2 at d35c5785333d, 1 watchdogs (synthetic bench at the declared requiredScale (crates/rightkit-tauri-shell/benches/budget.rs): 30 s idle, Windows 11 dev profile), synthetic,
rustright cargo test -p rightkit-tauri-shell --profile dev --bench budget --jobs 20 -- 30, 2026-10-18, evidence 61ea5d13ca34: resident memory 12.3 MiB, idle CPU 0.052%, wake-ups 6.862 per second, disk operations 0 operations per second. - Measured: darwin/arm64 (Mac18,5), rightkit-tauri-shell 0.3.2 at 8d96d8ca29df, 1 watchdogs (default hang watchdog on an idle Wry Tauri event loop with no windows; 5 s warm-up, 30 s idle), synthetic,
rustright cargo test -p rightkit-tauri-shell --profile release-iterate --bench budget -- 30, 2026-10-18, evidence ea35573730c3: resident memory 64.6 MiB, idle CPU 0.053%, wake-ups 5.121 per second, disk operations n/a (not measurable on this host). - Measured: darwin/arm64 (Mac18,5), rightkit-tauri-shell 0.3.3 at 15b3ce83bcc1, 1 watchdogs (default hang watchdog on an idle Wry Tauri event loop with no windows; 5 s warm-up, 30 s idle), synthetic,
rustright cargo test -p rightkit-tauri-shell --profile release-iterate --bench budget -- 30, 2026-10-18, evidence d44a1e2cb505: resident memory 64.6 MiB, idle CPU 0.022%, wake-ups 4.523 per second, disk operations n/a (not measurable on this host).
Engine policy & independent trays (G15–G18)
Enable Cargo feature engine, then opt into native policy:
use Duration;
use ;
let spec = new;
let policy = default
.allow_methods
.method_timeout
.restart_policy
.supervisor_config;
let shell = builder
.engine_with_policy.build;
None allowlist permits every method; an empty allowlist denies every method.
Names match exactly, before any client acquisition or spawn. Missing deadline entries use
EngineSpec::call_timeout. Without supervision, None means no call deadline. Opting into
restart_policy or supervisor_config uses rightkit-job-queue::EngineSupervisor; with
call_timeout: None, its configured RPC timeout applies (120 seconds by default).
supervisor_config runs after shell defaults & can set contract checks, readiness waits,
stderr/protocol sinks, initial-failure behavior & frame limits.
EnginePolicy::keep_alive_on_timeout = true abandons only caller wait; default supervised
timeout kills wedged engine. Automatic, on-demand & disabled restarts share job-queue's bounded
generation/budget rules. Failed in-flight calls are returned without replay. engine(spec)
keeps legacy recovery.
Register commands through rightkit_tauri_shell::handler![] or add only
commands::rk_shell_engine_call_tagged & commands::rk_shell_engine_status to an existing
tauri::generate_handler!. Legacy rk_shell_engine_call keeps its signature & string errors;
it applies configured allowlist/deadlines too. Tagged command adds optional tag & returns
structured {code, message, origin} errors. Native callers use
EngineHost::call_tagged(method, params, tag, Arc<dyn Fn(EngineEvent) + Send + Sync>).
EVENT_ENGINE / renderer EVENTS.engine carry {call, id, tag, event} for progress.
Request ids originate in job-queue; caller tags are preserved independently for concurrent
calls. $unsolicited frames have null id/tag. $lifecycle frames carry current lifecycle
snapshot in event & raw transition diagnostics in supervisor; they have no request id.
rk_shell_engine_status replays current state as {state, hello, restarts, last_error} in
supervised mode (or null without an engine); legacy startup snapshots retain their old shape.
States: starting, ready, restarting, failed, stopped; legacy startup failures use
error. rk_shell_ready also replays state. Event forwarders stop during ordered shutdown.
Additional trays use independently owned handles; shell's primary tray stays available:
use Arc;
use ;
use ;
let demo = install_with_options?;
demo.update?;
// Retain demo in application state for updates; demo.icon() exposes native title/icon APIs.
Call during setup/on main thread. custom_only uses exactly provided rows, including empty
menus; TrayOptions::default() retains Show/custom/Quit order. Duplicate tray ids, empty or
duplicate item ids & standard-action id collisions are rejected. Logical ids may repeat across
trays: native ids are namespaced & handlers filter those exact ids, including after menu rebuilds.
Custom-only menus may use shell Show/Quit logical ids as ordinary custom actions. Renderer tray
events add trayId; native callback receives logical id. TrayHandle::update addresses only its
own tray. Existing tray::install, tray::update, TraySpec & TrayUpdate remain unchanged.
Verification: first cargo build -p rightkit-tauri-shell --features engine --example shell_engine_fixture,
then cargo test -p rightkit-tauri-shell --features engine --all-targets exercises real-child
engine policy/integration tests; tests/tray_options.rs tests menu
planning without native windows. Native multi-tray rendering/clicks require desktop smoke test.
Tagged errors identify origin as engine or supervisor. Retry readiness codes only for
supervisor errors: an engine reply using down or not_ready may follow completed work.