English · 简体中文
The shared Rust engine used by RustClipSync Desktop on Windows/Linux and the Omaclipsync plugin. It provides protocol-3 direction control, durable synchronization, encrypted state, sensitive inboxes and optional conflict recovery. Clients supply platform adapters for clipboard access, storage and credentials.
Release notes · GitHub release
| Component | Matching release |
|---|---|
| Server | 3.0.0, wire protocol 3 |
| Shared core | 0.4.0 |
| Windows/Linux desktop | 0.5.0 |
| Omaclipsync | 0.4.0 |
Mobile apps and Fcitx input methods are released separately. Check their wire-protocol support before switching a shared server; older clients need server 2.0.0. Android currently implements the protocol independently in Kotlin.
| Capability | Behavior |
|---|---|
| Device direction and pause | Push only, receive only or bidirectional; pause retains the selection. Report the policy and wait for an ACK before data access. The server filters deliveries, with local guards retained in the core. |
| WebSocket control and HTTP fallback | Persistent authenticated WSS control; retry the identical operation over HTTPS if WSS fails. Session IDs and monotonic versions prevent delayed requests from restoring old settings. |
| Latest-only reconnect | Consume only an authorized per-device delivery after direction confirmation. The server owns the backfill window; clients do not hardcode it or fetch the global clipboard to bypass it. |
| Durable conflict recovery | An explicit client opt-in cancels/skips a conflicting upload, preserves the local clipboard and resumes new copies. Recovery survives a network failure or restart. |
| Private sensitive inbox | Keep the latest allowed marked-sensitive receive in encrypted state without writing the system clipboard. Disabling sensitive synchronization still permits ordinary receives. |
| Background receive APIs | Receive into the private inbox without clipboard access, or explicitly opt into clipboard writes without reading a snapshot. Hosts own scheduling and platform permissions. |
| Encrypted durable state | RCS1 AES-256-GCM state retains device identity, pending uploads, receive cursors, the apply journal and recovery progress across restarts. Stable message IDs and acknowledgments suppress retries and echoes. |
| Metadata notification worker | Long polling, coalesced notifications and connection backoff without copying clipboard bodies into the worker. Explicit shutdown/join and typed error chains support host lifecycle handling. |
These are library capabilities. Each client chooses its UI, permissions and opt-ins; adding an API here does not enable it automatically in every client. This crate does not provide a desktop UI, server or end-to-end encryption.
All network operations use /api/... and require wire protocol 3. This release deliberately rejects older servers and clients: deploy matching versions together, or keep old clients on the previous server. Device credentials and the encrypted-state format remain unchanged. Rust desktop clients and Omaclipsync consume exported snapshots of this repository; endpoint logic must be changed here and re-exported.
| Module | Responsibility |
|---|---|
direction |
Protocol version, direction/pause DTOs, WebSocket frames, ACKs, structured errors and mutation validation |
client / transport |
HTTPS registration and data, persistent WSS direction control with HTTP fallback, cancellation, acknowledgment and long polling; the transport interface supports offline tests |
state |
UUID, epoch, revision, outbox, receive cursor, apply journal, sensitive inbox and conflict-recovery marker; compatible with existing RCS1 AES-256-GCM state |
session |
Local direction/pause guards, latest-only synchronization, stable-ID retries, optional conflict recovery, sensitive policies, foreground/background receive and pre-apply checks |
worker |
Metadata long polling, notification coalescing and connection backoff; clipboard text is not copied to the polling thread |
Filter |
Regex filters for uploads and downloads |
adapters |
Interfaces for system clipboard access, atomic state storage and optional history |
The library has no Wayland, WinUI, Windows API, Secret Service, tray or file-path dependencies. Creating a session loads or creates encrypted state but does not connect to the network or read or write the clipboard. Hosts must serialize synchronization calls on a dedicated worker thread; do not perform blocking network operations on the UI thread.
The current library version, 0.4.0, identifies the shared Rust API independently of server and client product versions. The /api/ prefix identifies the current HTTP API without a version in its path; X-RCS-Protocol: 3 negotiates the required protocol.
- Obtain a random 32-byte key from the platform credential store: Secret Service on Linux or DPAPI CurrentUser on Windows.
- Implement
StateStorewith bounded reads, access restricted to the current user and atomic replacement; persist all bytes generated by the core. ReturnNoneonly when the file does not exist. Never silently recreate the identity after decryption fails. - Implement
Clipboardto return text and sensitivity markers and attach a source marker to writes. A successfulwritemeans the system clipboard accepted the content; storing it only in a private inbox must not be reported asapplied. - Use
NoHistoryor implementHistorySink. The core neither dictates a history storage format nor logs clipboard text. - Create a session with
SessionConfigandSession::open. Callset_sync_direction(SyncDirection::Bidirectional, false)(or the selected direction/pause state) beforecheck; the default unknown direction denies data. The watcher maintains a shared local event counter; record real events withrecord_local/record_clearand synchronize usingflush/pull.
On a state persistence error, pause and rebuild the session instead of continuing with in-memory state that may be ahead of disk. On first startup, do not mistake existing clipboard content for a new copy event. Notifications are wake-up signals; the epoch and revision returned by state determine synchronization.
Synchronization entry points check restored pending content against the current clipboard before sending. flush applies the current sensitive-content and regex policies to the latest queued value. An oversized new copy also supersedes the old value; uncertain uploads are resolved through cancellation before they are discarded. If a clipboard write returns an error, the session durably records it as skipped before returning the error, so later receives can continue without replaying the ambiguous write. A failure to save that recovery is a state persistence error and requires rebuilding the session.
Error, SyncError and ClipboardError implement Display and std::error::Error. Their display text omits underlying URLs and adapter-provided details; source() retains the typed causes for callers that need to inspect them. Worker::shutdown() requests a stop without waiting. Worker::join() requests a stop and waits for completion, returning any thread panic to the host; call it away from the UI thread. Dropping a worker still requests a stop without waiting. Custom Transport implementations must bound each operation, including the entire long-poll request, with a finite timeout; shutdown cannot interrupt an in-flight custom call.
- Turning off sensitive synchronization while the current clipboard is marked sensitive does not block normal receives. Receive-side snapshot checks treat the policy rejection as an unavailable text value without reading its body; actual clipboard I/O failures still stop the operation. Incoming marked text remains filtered when sensitive synchronization is disabled.
- Connects to the RustClipSync server using its protocol. Legacy SyncClipboard HTTP/SignalR transport is not implemented.
- A UUID identifies one copy event and remains stable across retries. Revisions determine order; epochs separate server resets. Client time is used only for retry expiry, not ordering or uniqueness.
- Automatic uploads do not silently overwrite conflicts; offline synchronization keeps only the latest value. When an upload outcome is uncertain, retain the request and resolve or cancel it before processing a newer value. Safe conflict recovery requires an explicit host policy.
- Persistent cursors and acknowledgments prevent replay. A message whose clipboard write outcome is unknown after a crash is recovered as
skipped; exactly-once clipboard side effects are not guaranteed. - The host supplies sensitive markers; marked-sensitive synchronization should be disabled by default. Neither markers nor regular expressions can identify all sensitive information.
- TLS certificates are verified by default. Plain HTTP, credentials embedded in URLs and redirects are rejected. The explicit
insecureoption is intended only for controlled tests or self-signed environments. - State encryption protects files at rest, not end-to-end traffic; the server can read synchronized text. Text, keys and pairing passwords in memory are not currently guaranteed to be cleared immediately.
- Text is limited to 10 MiB per item and state to 64 MiB. Long-poll worker shutdown waits for bounded direction negotiation plus the current HTTP request's 30-second timeout; hosts must avoid repeatedly creating overlapping workers.
direction::SyncDirection has PushOnly, ReceiveOnly and Bidirectional,
serialized as PUSH_ONLY, RECEIVE_ONLY, BIDIRECTIONAL. Pause is a separate
boolean and retains the chosen direction. Unknown, revoked and paused states
receive no messages; receive-only and paused sessions cannot publish. The core
retains local send/receive guards even when reporting fails.
| State | Publish | Receive |
|---|---|---|
| Push only | Yes | No |
| Receive only | No | Yes |
| Bidirectional | Yes | Yes |
| Paused, unknown or revoked | No | No |
use rustclipsync_core::direction::SyncDirection;
// After Session::open, before check/flush/pull:
// session.set_sync_direction(SyncDirection::Bidirectional, false)?;
// session.check()?; // register -> direction ACK -> metadata
// session.set_sync_direction(SyncDirection::ReceiveOnly, false)?;
// session.set_sync_direction(SyncDirection::ReceiveOnly, true)?; // pause
// session.revoke_sync_direction()?; // explicit logout, not ordinary shutdownThe host must increment its local event generation before changing direction,
pausing or logging out, stop its old notification worker, and serialize session
calls on its background thread. A failed update retains the new local policy
and reports an error; a failed logout is unconfirmed, never a successful
revocation. Ordinary Worker::shutdown, process close or network loss does not
revoke direction, preserving offline backfill. Explicit re-pairing uses
re_pair_with_direction(direction, paused); failed old-session revocation does
not destroy the old identity.
The HTTPS client keeps one bounded control WebSocket at /api/ws, authenticates
with Bearer and the protocol header (never tokens in URLs), and sends tagged
SyncDirectionUpdate / SyncDirectionRevoke frames. It validates the matching
SyncDirectionAck device/session/version. WS connection or I/O failure retries
the identical operation through HTTP, preserving idempotency when a WS ACK
was lost. A failed HTTP retry remains pending. Direction control uses a separate
mutex; the lock is not held across the HTTP long poll. The poll is capped at
30 seconds; an initial discovery/control/fallback can add up to 40 seconds, so
worker shutdown must remain off the UI thread. WebSocket frames are
limited to 4096 bytes with a 10-second I/O deadline, including slow fragments. HTTPS discovery supplies the resolved peer address,
avoiding an additional unbounded system DNS lookup. With an HTTP proxy that
cannot carry the control socket, the same authenticated HTTP fallback is used.
HTTP fallback and state discovery use:
GET /api/devices/{deviceId}/sync-direction: read the current version without activating state or refreshing TTL.PUTat that path:{sessionId, version, direction, paused, connect}. A fresh canonical UUID andconnect:trueclaim a session via GET then CAS=current+1; ordinary updates use the current session andconnect:false.DELETEat that path:{sessionId, version}. Logout retains a versioned tombstone; an exact retry is idempotent. Delayed old updates cannot revive it.
Example update (the WebSocket variant adds type: "SyncDirectionUpdate"):
{"sessionId":"00000000-0000-4000-8000-000000000001","version":1,"direction":"BIDIRECTIONAL","paused":false,"connect":true}Every sync response must advertise X-RCS-Protocol: 3. HTTP 426 with
PROTOCOL_UNSUPPORTED is terminal, including an older peer that omits the
header. Structured errors also distinguish UNAUTHENTICATED, DEVICE_MISMATCH,
VERSION_STALE, SESSION_REVOKED, SESSION_OFFLINE, RATE_LIMITED, INVALID_DIRECTION and
DIRECTION_FORBIDDEN. Fenced sessions stop old workers; they do not silently
claim another session to replay stale settings. An explicit host connection or
policy update starts a new attempt. SESSION_OFFLINE means only the current
session lease expired; the core reconnects with fresh GET+CAS without blindly
retrying the old data request. Errors from an older in-flight session cannot
reset or fence a newer accepted session. Data requests carry the accepted
X-RCS-Sync-Session; registration, direction control and cancellation do not.
A reconnect reports the latest direction and waits for ACK before reading data. The server can replay at most one eligible message per device from its administrator-configured window. The client does not contain a backfill-window constant or fetch the global current message as a fallback. Window values and zero/expiry semantics belong to the server configuration. Direction TTL is server-owned: valid reporting/login/activity refreshes it; 30-day inactivity revokes eligibility and requires a new report.
RemoteState.revision remains the global upload-conflict revision, while nullable
deliveryRevision identifies an authorized per-device message. The notification
worker keeps a separate monotonic observed-global cursor so filtered metadata
does not cause a hot polling loop; host updates cannot roll that cursor back. A valid delivery
older than the global revision is processed before advancing the local cursor.
An absent delivery may advance observation, but never rebases a queued upload.
Bodies require current direction/session authorization on the server. The
existing durable apply journal, skipped acknowledgements, sensitive inbox,
local generation checks and explicit conflict policy remain in effect.
Custom Transport implementations must implement set_sync_direction,
ensure_sync_direction and revoke_sync_direction, in addition to the existing
HTTP operations. The new public methods/types and RemoteState.delivery_revision
are a Rust API break represented by crate 0.4.0.
With marked-sensitive synchronization explicitly enabled, clients can call
Session::use_sensitive_inbox(true) to keep incoming sensitive text and the receive
cursor in one encrypted state commit. The result is Outcome::Stored; the server
acknowledgment is skipped, never a claim that the system clipboard was written.
Only the latest inbox item is retained. clear_inbox(id) removes the matching item
without resetting the cursor, so clearing it does not trigger a replay.
Without the private inbox opt-in, foreground receives use the selected sensitive
policy and normal clipboard path. With sensitive synchronization disabled, marked
messages remain filtered; an already-sensitive local clipboard does not block
ordinary receives. HistorySink::record_marked lets hosts withhold sensitive text
from ordinary history. Notification identity never contains inbox text.
Older encrypted files without an inbox or recovery marker load with empty/false defaults. The state limit is 64 MiB to accommodate pending, replacement and inbox items; the per-item text limit remains 10 MiB.
| API | Clipboard access | Receive policy |
|---|---|---|
pull(observed, now) |
Reads a snapshot and may write | Foreground receive with snapshot and generation checks |
pull_inbox(observed) |
Neither reads nor writes | Marked-sensitive messages only, with the encrypted inbox enabled; ordinary messages remain available for foreground receive |
pull_background(observed, true) |
May write; never reads | Ordinary messages may be written; marked messages still obey the skip/inbox/write policy |
pull_background(observed, false) is equivalent to pull_inbox(observed).
All paths retain direction, sensitive and regex filtering, validation, durable
state and acknowledgment checks. Pairing and restored-outbox validation must
finish through foreground check first; pending uploads and conflict recovery
remain foreground work.
Background writes require a separate explicit host opt-in. Without a readable clipboard snapshot, only the host's generation counter protects a newer local copy, so the UI must explain that another app's recent clipboard may be replaced. The core provides no background runtime, OS permission or scheduling guarantee.
The default automatic-upload policy still returns SyncError::Conflict without
overwriting the remote value. Hosts can explicitly choose safe skipping instead:
- After
check, inspecthas_observed_upload_conflict()before normal receive. This also detects a restored, previously attempted outbox with an older baseline. - On that condition or an upload conflict, call
recover_upload_conflict(observed)on the serialized session thread. - The core persists the recovery decision, resolves the old upload through cancellation, drops queued copies, and skips the then-current authorized remote delivery. It preserves the local clipboard and never rebases or re-uploads the rejected text. Cancellation cannot undo an upload already committed by the server.
- If interrupted,
flushresumes the durable cancellation/skip after network recovery or restart. The host discards watcher events observed during recovery and accepts fresh copy events only after recovery completes.
Storage failures, an epoch change and credential errors still stop synchronization; they are not ordinary conflicts. Omaclipsync 0.4.0 opts into this recovery. Other integrations, including the desktop client, retain their own conflict policy.
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
cargo fmt --all -- --check
cargo doc --locked --no-depsWith a server test binary available, run the HTTPS protocol integration test:
python3 tests/server_e2e.py ../rustclipsync-server/target/release/rustclipsync-serverThis requires Python 3 and OpenSSL. It launches a temporary server, database and loopback-only HTTPS proxy, using in-memory clipboard and key-store substitutes without touching the user's desktop. Unit tests include an encrypted state migration fixture generated by unmodified Omaclipsync 0.2.0 code. See VALIDATION.md for scope and platform limitations.
For local development with adjacent repositories:
rustclipsync-core = { version = "=0.4.0", path = "../rustclipsync-core" }The source repository is public under the MIT license and is not published on crates.io. To keep standalone Omaclipsync checkouts and source packages buildable, Omaclipsync consumes an automatically exported snapshot in core/vendor/rustclipsync-core:
python3 tools/export_source.py ../omarchy-plugins-dev/io.github.infinifar.omaclipsync/core/vendor/rustclipsync-core
python3 tools/export_source.py ../omarchy-plugins-dev/io.github.infinifar.omaclipsync/core/vendor/rustclipsync-core --checkThe export manifest records the source version, original extraction commit and SHA-256 of each file. Do not edit consumer snapshots directly: change this repository, run tests, re-export, then update the consumer's Cargo.lock. A future library release can move consumers to a pinned version or Git commit instead of a floating branch.
The Windows and Linux desktop client is RustClipSync Desktop, with WinUI 3 on Windows 11 and a standalone Qt 6/QML interface on Linux. Linux x86_64 packages target Debian 12, Ubuntu 24.04, Fedora 43 and Arch/Omarchy, without requiring Omarchy or Quickshell. See the downloads and Linux guide. X11 is supported; Wayland requires data-control support, and GNOME/Wayland sessions without it are currently unsupported.
The Omaclipsync plugin provides integration with the Omarchy shell. Both desktop clients and the plugin reuse this core through platform adapters. Android currently implements the same protocol in Kotlin without JNI.
MIT license. The original implementation was extracted from Omaclipsync 0.2.0 in
Infinifar/omaclipsync, baseline commit a8c330136886ad547dfadf386bb66901b3bbc45c.
New cross-platform interfaces, tests and export tools are maintained here.