Skip to main content

Crate rightkit_daemon

Crate rightkit_daemon 

Source
Expand description

rightkit-daemon: the one discovery and authentication convention for local Right Suite services (the shared speech service, the in-app control server, right-ctl).

  • Discovery - <suite>/runtime/<service>/service.json, written atomically after the endpoint is listening, removed on clean exit. A record whose endpoint does not answer is stale and ignored.
  • Ranking and takeover - one host per service per suite. A would-be host that outranks the incumbent (lower rank wins) writes takeover.json; the incumbent stops admitting work, finishes or hands off in-flight requests with the retryable ERROR_HOST_HANDOFF at their next boundary, and releases. Clients re-read discovery and resubmit. The incumbent looks for a request from another process once a second (a stat of takeover.json, no thread polls faster), so a takeover starts up to about a second after the request is written.
  • Authentication - no TCP and no bearer tokens. Unix: a socket in a 0700 directory, mode 0600, and the kernel-reported peer uid (getpeereid on macOS and the BSDs, SO_PEERCRED on Linux) checked on every accept and by every client (verify_peer_is_current_user); a platform that cannot check refuses every peer. Windows: a named pipe whose DACL grants only the current user’s SID ([owner_only_security_attributes]), remote clients rejected, first-instance flag set against name squatting, a bounded instance count (HostOptions::max_pipe_instances), the client’s process-token SID checked on every accept, and clients opening with SECURITY_IDENTIFICATION and checking the server’s SID. See the README threat model.
  • Verifying the host - the transport proves a host runs as the current user, not which program it is. Client::connect_expecting takes a HostExpectation (the executable the host may be, for example a sibling inside the client’s own app bundle), asks the kernel who is on the listening end of the connected socket (macOS LOCAL_PEERPID and proc_pidpath, Linux SO_PEERCRED and /proc/<pid>/exe), and refuses anyone else with a typed HostIdentityError before the hello or any request is written. Client::connect stays unauthenticated-host. ClientConfig::ignore_env_overrides makes the suite root come only from the account database, so a launcher’s environment cannot redirect a release client. See the README, “Verifying the host”.
  • Per-app request allowlist - each client names its app in hello; an app may call only the methods listed for it (the generalisation of HeardRight’s SERVICE_ALLOWED_SCHEMAS). The app label is an allowlist key, not a security boundary against code already running as the same user.
  • Fail-closed roots - Suite::resolve errors when no absolute app-data root can be determined; it never falls back to the working directory. Suite::per_run_temp is the explicit opt-in alternative.

Wire format: bounded JSON lines (rightkit-sidecar), the same frames as the rightkit-job-queue engine contract: hello, request{id,method,params}, event{id,event}, response{id,ok,result|error}.

Re-exports§

pub use private::exit_when_stdin_closes;
pub use private::watch_reader_eof;
pub use private::watch_stdin_eof;
pub use private::PrivateChild;
pub use private::PrivateSecret;
pub use private::SecretError;
pub use private::SECRET_BYTES;
pub use private::SECRET_HEX_LEN;

Modules§

chunk
Chunked calls: messages larger than one frame (issue #294).
contract
The one request contract, independent of transport.
mode
The one runtime decision between a release run and a test run (M21), and the only sanctioned reader of a test or override selector (M23).
private
Private per-run child service: a secret passed over stdin, checked in constant time on every frame, and a child that exits when stdin closes.
stop
Stop fan-in for a daemon host (issue #375): one StopSource that several inputs may trigger (an OS stop signal, the parent closing this process’s stdin, or the program itself), and StopTokens that the host’s threads wait on. The first cause wins and later triggers are ignored.
supervisor
A supervisor loop for a daemon-pipe child (issue #295).
ticker
Periodic work and a heartbeat record for a daemon host (issue #376).

Macros§

test_selector
See the test-mode form: this one drops the name.

Structs§

Allowlist
Which methods each calling app may use. "*" as an app key is the default for apps with no entry of their own; "*" as a method allows every method for that app. ping is always allowed. An empty allowlist denies everyone.
AppRoot
<Orthic Labs>/<App>, created private.
CallContext
What a handler knows about the request it is serving.
Client
A connection to one host of a service. One request in flight at a time; open a second Client for parallel calls. Dropping it closes the connection, which the host treats as a disconnect (jobs started with cancel_on_disconnect are cancelled).
ClientConfig
What a client needs to find and call one service, including how much it trusts its environment. Pass it to Client::connect_expecting (verified host) or Client::connect_config (hardened root, unverified host).
Conn
An accepted or connected duplex stream. peer is who the OS reports on the other end.
DiscoverOptions
Options for discovery. DiscoverOptions::default() reproduces the original behaviour.
EndpointError
The endpoint for a service cannot be named within the OS socket path limit, even at the short fallback location. Names the offending path, its length and the limit.
HostExpectation
Which program the client is willing to talk to, passed to Client::connect_expecting.
HostIdentityError
The process behind the endpoint is not the host the caller expected. No hello and no request frame was written to it.
HostOptions
Host-side transport options, passed to host_with_options. HostOptions::default() is what host uses.
HostProcess
The process the OS says is on the listening end of a connection this client opened, as it was when every check passed. Returned by Client::verified_host and handed to a caller-supplied verifier (HostExpectation::with_verifier).
Hosted
A running host. Dropping it releases the service.
InstalledApp
Installed Right Suite applications and their engine executable.
Listener
PeerIdentity
Who the kernel says is on the other end of a connection.
RetryPolicy
Reconnect-and-resubmit policy for retryable errors (handoff, host restart).
Service
A running service: the local host, the remote daemon, or both, over one handler.
ServiceBuilder
ServiceConfig
ServiceError
Typed wire error. retryable means resubmitting the request can succeed.
ServiceRecord
<suite>/runtime/<service>/service.json.
ServiceStatus
Runtime figures of a running host, computed on call by Hosted::status.
SpawnClaim
The claim around one process spawn attempt, backed by an OS exclusive lock on spawn.claim. The lock lives as long as the open file: the kernel releases it when the owner dies (any signal), and there is no time-based eviction of a live owner. The file is never unlinked, so every contender always locks the same inode; Drop closes only this claim’s own handle.
Stream
A raw duplex byte stream to an endpoint, for callers that speak their own line protocol over the shared transport (HeardRight’s v1 EngineFrame dialect, which has no hello). Same current-user-only transport as crate::Client.
Suite
The per-user suite directory that holds runtime/<service>/service.json and other shared state. Always absolute.
TakeoverRequest
<suite>/runtime/<service>/takeover.json: written by a would-be host that outranks the incumbent; deleted by the requester once hosting.
UninstallEntry
One uninstall registry row, independent of the Windows registry API so it can be tested from captured reg query output.
WriterLock
Held while this process is the single writer of an app root. Released on drop or process death (OS lock).

Enums§

Accepted
Outcome of Listener::accept: an authenticated connection, a rejected peer, or one that left before it could be identified.
AdoptOrSpawn
AppRootError
BuildError
Why ServiceBuilder::start failed.
Completeness
Whether an installed app is usable as a service host.
ConnectError
Why Client::connect_expecting did not return a client.
HostError
HostIdentityReason
Why a host was refused. Always the host’s identity, never a connection problem.
HostPlan
Outcome of plan_host for a process of a given rank.
MigrationOutcome
What AppRoot::migrate_from did.
PeerError
Why a peer was not accepted as the current user.
ServiceEvent
Content-free lifecycle events for the host’s journal.
SpawnClaimError
SuiteError

Constants§

DEFAULT_DEADLINE
Default per-request deadline (connect + hello, and each call’s write + read). Override with Client::connect_with / Client::set_deadline; None disables it.
DEFAULT_FRAME_LIMIT
DEFAULT_MAX_CONNECTIONS
Default for HostOptions::max_connections.
DEFAULT_MAX_PIPE_INSTANCES
Default for HostOptions::max_pipe_instances.
DEFAULT_WRITE_TIMEOUT
Default for HostOptions::write_timeout: how long one frame write plus flush to a client may take before the host closes that connection. Matches the client’s default call deadline.
ENV_SUITE_ROOT
Overrides the platform default (tests, isolated dev profiles). Must be absolute. RIGHT_SUITE_DIR and ORTHIC_SHARED_DIR are honoured as legacy aliases.
ERROR_BAD_CHUNK
A chunk frame (rk.upload, rk.fetch) was malformed, out of order, repeated or for an upload or result that is gone. Not retryable as sent: start the whole transfer again.
ERROR_CONNECTION_CLOSED
The connection dropped before a response arrived. Retryable.
ERROR_FORBIDDEN
The caller is known but lacks the scope for this method.
ERROR_HOST_HANDOFF
The host is handing the service to a higher-ranked host. Retryable: re-read discovery and resubmit the whole request.
ERROR_HOST_IDENTITY
The process behind the endpoint is not the host the caller expected (Client::connect_expecting). Not retryable: reconnecting reaches the same impostor, and no application byte was sent to it.
ERROR_NOT_ALLOWED
The calling app may not call this method (or is not on the allowlist).
ERROR_REQUEST_TOO_LARGE
A request is larger than the host will assemble from chunks (HostOptions::max_message), or than one frame when the host does not chunk. Not retryable. Ask for less.
ERROR_RESPONSE_TOO_LARGE
The handler’s result did not fit the host’s frame limit, so it was not sent. Not retryable: the same request produces the same result. Ask for less (a page, a smaller body).
ERROR_REVISION_CONFLICT
A write named a stale revision.
ERROR_SUITE_UNRESOLVED
The suite root could not be resolved, or a ClientConfig that ignores environment overrides was handed a suite that came from the environment. Not retryable.
ERROR_TIMEOUT
The per-request deadline (connect + write + read) expired and the connection was aborted. Not retryable: the request may already have run on the host, so it is never replayed automatically.
ERROR_UNAUTHENTICATED
The caller presented no valid credential (a device that is unknown, or whose signature failed).
ERROR_UNAVAILABLE
No live host for the service. Retryable.
KNOWN_APPSDeprecated
The original Right Suite table. Deprecated: the crate hard-coded one product suite’s names. Products pass their own (app name, engine executable) registry to discover_apps_in. Kept with identical contents so existing callers behave the same in this release.
MAX_APP_NAME_LEN
The longest app name accepted, in bytes.
MAX_SERVICE_NAME_LEN
The longest service name host accepts, in bytes.
MAX_UNIX_SOCKET_PATH
Longest Unix socket path this crate will bind: sockaddr_un.sun_path is 104 bytes on macOS and 108 on Linux, each including the trailing NUL, so 103 is the portable ceiling.
MIGRATION_MARKER
The commit marker written at the app root once a migration is verified.
PROTOCOL_MAJOR
Bumped on any incompatible change to discovery files, frames or the allowlist semantics. A client rejects a different major.
PROTOCOL_MINOR
RANK_UNKNOWN
Rank of a record or takeover request that carries none (HeardRight’s service.json / takeover.json name only a role). Never wins a takeover: a host resolves it through crate::ServiceConfig::known_roles or treats the incumbent as unbeatable.
SHARED_DIR_NAME
The shared-data subdirectory under SUITE_DIR_NAME. HeardRight, ScrapeRight and CutRight already root their shared model store and speech discovery here (.../Orthic Labs/shared), so the one convention adopts it.
SUITE_DIR_NAME

Traits§

Handler

Functions§

adopt_or_spawn
Adopt only a live, reachable record whose PID generation still matches.
clear_takeover_if_owner
Remove takeover.json only if pid wrote it.
discover_appsDeprecated
Discover the built-in Right Suite apps. Deprecated: use discover_apps_in.
discover_apps_in
Discover installed apps from the caller’s (app bundle or display name, engine executable) registry. The first complete match per app wins.
discover_apps_in_with
discover_apps_in with explicit DiscoverOptions.
discover_macos
Scan roots in order; first complete bundle wins per app.
discover_macos_with
discover_macos with explicit DiscoverOptions. With include_incomplete, an app that has a bundle directory but no engine in any root is returned once, from the first root that has its bundle, as Completeness::EngineMissing.
discover_windows
Discover complete Windows installs only. Equivalent to discover_windows_with with DiscoverOptions::default.
discover_windows_with
Windows discovery with explicit options. exists checks the engine file and dir_exists checks the install directory. With include_incomplete, an entry whose directory exists but whose engine is missing is returned as Completeness::EngineMissing. A complete install from any entry of the same app always wins.
endpoint_candidates
Endpoints a client may find a live host on, in order: the current endpoint, then (Unix only) the pre-upgrade long fallback name when it would have fitted. Compatibility decision: for one release a new client still finds an old host on the old fallback name; that name is connect-only and is never bound. Where the old name never fitted the limit no old host could have bound it, so there is no second candidate.
endpoint_for
try_endpoint_for for callers that cannot handle the error: on failure it returns the path that did not fit, so a bind attempt fails with the OS error rather than panicking here.
find_live_endpoint
The first of endpoint_candidates that answers a connect.
host
Host cfg.service on suite: discover, rank, take over if outranking, bind the current-user-only endpoint, publish service.json. Chunked calls (rk.upload) are off unless the options set HostOptions::max_message.
host_with_options
host with explicit HostOptions.
open_stream
Connect to endpoint. On Windows a busy pipe is retried up to 3 s and a not-yet-created one up to 1 s (HeardRight’s behaviour).
open_stream_expecting
open_stream that first verifies the host is the program expect names, before a byte is written (see Client::connect_expecting for the checks and what each platform verifies). A host that is not the expected one is ConnectError::HostIdentity; a host that cannot be reached is ConnectError::Service with the retryable service_unavailable.
parse_plist_string
Parse a plain XML plist without requiring a plist dependency. Binary plist files return None, while the app remains discoverable.
parse_reg_query
Parse reg query ... /s output. Unknown values and malformed rows are ignored; keys are grouped by their registry-key heading.
pid_alive
Is process pid alive? A dead pid marks a discovery record stale and a takeover request abandoned. Hardened from HeardRight’s version: pid 0 and pids that do not fit pid_t are never alive (they would signal process groups), and on Windows an exited process whose handle is still open is dead.
plan_host
Decide how a process of rank my_rank gets a host for service. A live record whose rank cannot be resolved through known_roles counts as absent. installed_engine is the executable the caller may start when nothing is live; it is used only when it exists.
poll_until
Poll ready every 100 ms until it holds (true) or wait elapses (false). Checks before the first sleep.
probe_endpoint
Does something answer at endpoint? (Connect, then drop.)
publish_takeover
Publish takeover.json for this process as role (with rank), atomically.
read_live_record
The record only when its owner process is alive (HeardRight’s staleness rule: a dead pid means the record is stale). Does not probe the endpoint; connecting does that and reports service_unavailable. Unreadable is None; see try_read_live_record.
read_record
The published record. A record that is absent, unparseable or unreadable is None: use try_read_record where “unreadable” must not be taken for “no service”.
read_takeover
The pending takeover request. Absent, unparseable and unreadable are all None: use try_read_takeover where “unreadable” must not be taken for “no request”.
record_path
record_process_matches
Check both liveness and process generation, protecting discovery from PID reuse.
refuse_build_tool_real_data
Refuse a development run that the build tool started against the real suite root (issue #384).
resolve_install_dir
resolve_rank
Rank of a live record or takeover request: its own rank when it writes one, else known_roles by its role. None when neither resolves; such a peer is never taken over from and never obliges a handoff.
should_request_takeover
Takeover is requested only against a live, lower-ranked host held by another process. incumbent is the live record’s (resolved rank, pid).
spawn_detached
Start program detached: no inherited stdio, its own process group on Unix, and on Windows no console window (CREATE_NO_WINDOW | DETACHED_PROCESS) so a GUI host never flashes one. The child is not waited for.
takeover_obliges_yield
Does a pending takeover request (resolved rank, pid) oblige a host of rank my_rank to yield? Only a live, other-process, outranking requester does.
takeover_path
try_adopt_or_spawn
adopt_or_spawn with Err when service.json exists but could not be read: whether a live host exists is then unknown, so look again rather than spawn.
try_endpoint_for
Endpoint for service on suite, or the typed reason none can be named. Windows: a pipe name that carries the user and a hash of the suite root, so an isolated suite never collides with the user’s installed host (at most 96 characters for a 48-byte service name and a 20-character user, far under the 256-character pipe name limit). Unix: a socket in the private service dir, or, when that path would exceed the sockaddr_un limit, a bounded rk-<tag>-<hash>.sock in the per-user temp dir.
try_plan_host
plan_host with Err when service.json exists but could not be read. Whether a live host exists is then unknown, so no plan is made: plan_host would answer HostMyself or StartInstalled, which starts a second host beside a live one.
try_read_live_record
read_live_record with Err for a record that could not be read.
try_read_record
The published record, telling “absent” from “unreadable”.
try_read_takeover
The pending takeover request, telling “absent” from “unreadable” as try_read_record does. Err means whether a request exists is unknown: a host must not conclude there is none.
valid_app_name
Whether s is an app name: 1..=MAX_APP_NAME_LEN bytes of ASCII letters, digits, -, _ and .; starts and ends with a letter or digit (so no ., .., hidden or trailing-dot names); no separators; not a Windows reserved device name (with or without an extension). Case is preserved on disk but is significant for identity on every platform: two names that differ only by case are refused as conflicting by AppRoot::new when one already exists.
valid_service_name
Whether s is a service name host accepts: non-empty, at most MAX_SERVICE_NAME_LEN bytes, and only ASCII lowercase letters, digits and -. This is the single rule host enforces.
verify_peer_is_current_user
Verify the peer of a connected Unix stream is the current user.
wait_for_claim_winner
Wait for the winner of a competing claim to publish a matching live record.
write_atomic
Atomic, durable, owner-only replace (rightkit_fs::atomic_write_private: unique temp file in the same directory, fsync, rename, directory fsync). The parent directory must exist.
write_takeover
Publish a takeover request (atomic, owner-only).

Type Aliases§

Emit
Sends an event frame for the request being handled.